แนะนำ Synx: การซิงก์ไฟล์สำหรับการพัฒนาแบบรีโมต

การพัฒนาแบบรีโมตลงเอยที่รูปทรงเดียวกันเสมอ และถ้าคุณเคยผ่านมันมา คุณจะจำมันได้จากประโยคเดียว: เครื่องที่คุณใช้คิด กับเครื่องที่งานจริงรันอยู่ ไม่ใช่เครื่องเดียวกัน

แล็ปท็อปคือที่ที่คุณใช้คิด มันมีเอดิเตอร์ที่คีย์บอร์ดชอร์ตคัตฝังอยู่ในนิ้วคุณ มีธีมของคุณ fuzzy finder ของคุณ เครื่องมือดูดิฟฟ์ของคุณ คลิปบอร์ดของคุณ language server ที่ปรับจูนไว้ตรงใจคุณเป๊ะ และเอเจนต์ AI ที่คุณชี้ให้มันดูโปรเจกต์นี้ มันคือเครื่องเดียวที่คุณเร็วจริง ๆ

เซิร์ฟเวอร์คือที่ที่งานเกิดขึ้น หกสิบสี่คอร์แทนที่จะเป็นแปด แรมก้อนนั้น หรือ GPU หรือเคอร์เนลตัวเดียวที่ไดรเวอร์ของคุณบิลด์ขึ้น หรือเครือข่ายส่วนตัวที่เอื้อมถึงฐานข้อมูล มันคือเครื่องที่บิลด์ release เสร็จในเก้าสิบวินาทีแทนที่จะเป็นสิบเอ็ดนาที ที่ชุดเทสต์กระจายไปทุกคอร์ ที่คอนเทนเนอร์ตรงกับโปรดักชันเพราะมันสร้างจากอิมเมจเดียวกัน

ทั้งสองทำงานแทนกันไม่ได้ และคุณก็ไม่ได้อยากให้ทำ คุณอยากแก้โค้ดตรงนี้ต่อไป และบิลด์ตรงโน้นต่อไป

ซึ่งย่อการพัฒนาแบบรีโมตทั้งหมดลงเหลือคำถามที่ไม่หวือหวาข้อเดียว: ไฟล์เดินทางจากฝั่งหนึ่งไปอีกฝั่งได้อย่างไร อย่างต่อเนื่อง ในทั้งสองทิศทาง และเร็วพอจนคุณลืมไปเลยว่ามันกำลังเกิดขึ้น? ทำข้อนี้ให้ถูก แล้วทั้งชุดเครื่องมือจะหายไป — คุณพิมพ์ คุณกดเซฟ คุณสลับไปเทอร์มินัลที่บังเอิญอยู่คนละทวีป แล้วมันก็บิลด์ ทำข้อนี้ผิด แล้วคุณจะใช้ทั้งวันเป็นพนักงานส่งของให้ซอร์สโค้ดของตัวเอง


คำตอบที่เอาไม่อยู่

ก็ทำงานบนเครื่องนั้นเลยสิ SSH เข้าไป เปิด vim แล้วปัญหาก็ระเหยไป — มีสำเนาเดียวตลอด บางคนมีความสุขกับแบบนี้จริง ๆ และผมก็ไม่คิดจะไปเถียงเขา แต่ราคาที่จ่ายคือสภาพแวดล้อมบนเครื่องคุณทั้งหมด: คอนฟิกเอดิเตอร์ ส่วนขยาย git client แบบกราฟิก คลิปบอร์ด เครื่องมือแคปหน้าจอ ผู้ช่วย AI ที่รันบนเครื่องคุณเอง และตอนนี้ทุกการกดแป้นอยู่หลังเครือข่าย บนไฟเบอร์ก็ไม่เป็นไร บนไวไฟโรงแรมคุณจะรู้สึกได้ถึงทุกตัวอักษรที่ร่วงลงมา

เมานต์รีโมตผ่านเครือข่าย SSHFS เปลี่ยนไดเรกทอรีรีโมตให้เป็นพาธบนเครื่อง ซึ่งฟังดูตรงกับที่คุณต้องการเป๊ะ จนกระทั่งคุณเห็นว่าอะไรกำลังวิ่งอยู่บนสาย: ทุก stat() ตัวเฝ้าไฟล์ของเอดิเตอร์ ตัวทำดัชนีของ language server git status ของคุณ — แต่ละตัวเดินผ่านพาธนับพัน และทุกการเดินคือการวิ่งไปกลับ รายงานตรงกันหมด: ประสิทธิภาพร่วงเป็นหน้าผาทันทีที่มีอะไรแตะไฟล์ทีละมาก ๆ อย่าง git checkout หรือ npm install บน macOS ยิ่งแย่ เพราะเรื่อง FUSE ที่นั่นกระอักกระอ่วนมาหลายปี นามธรรมนี้น่ารักจนถึงวินาทีที่ความหน่วงทำให้มันใช้ไม่ได้พอดี

rsync ในลูป คือการแก้ขัดแบบซื่อ ๆ ที่ทุกคนเคยเขียนอย่างน้อยหนึ่งครั้ง: fswatch | rsync หรือ while true; sleep 1 สำหรับสแน็ปช็อตทิศทางเดียวมันถูกต้อง แต่พอเป็นแบบสดมันเสื่อม — ไม่ก็ยิงทุกการกดแป้นจนถล่มลิงก์ ไม่ก็รวมเป็นชุดแล้วตามหลังการเซฟของคุณ มันเป็นทิศทางเดียวโดยการออกแบบ ดังนั้นถ้าคุณอยากให้ไฟล์ที่ถูกสร้างหรือผลลัพธ์การบิลด์เดินทางกลับมา คุณก็รันตัวที่สอง และตอนนี้สองโพรเซสที่ไม่มีความเข้าใจร่วมกันว่าอะไรคือความจริงก็แข่งกันอยู่บนต้นไม้เดียวกัน ที่คนกลัวกันน่ะถูกแล้ว: rsync ที่ใส่แฟล็กผิดในทิศทางผิดลบงานจริง ๆ ทิ้ง

ปล่อยให้ IDE จัดการ VS Code Remote-SSH และ JetBrains Gateway แก้เรื่องนี้ได้อย่างถูกต้อง — สำหรับเอดิเตอร์: มีคอมโพเนนต์ฝั่งเซิร์ฟเวอร์รันบนรีโมต ส่วน UI อยู่บนเครื่องคุณ ถ้าโลกทั้งใบของคุณอยู่ใน IDE ตัวเดียว นี่คือคำตอบที่ดี แต่ไฟล์ก็ยังคงมีอยู่แค่ฝั่งโน้น ทุกอย่างที่อยู่ นอก เอดิเตอร์จึงตาบอด: เทอร์มินัลของคุณ git client แยกต่างหากของคุณ สคริปต์บนเครื่องคุณ เอเจนต์เขียนโค้ดที่รันอยู่บนแล็ปท็อปคุณ อีกทั้งคุณยังผูกตัวเองไว้กับโปรโตคอลรีโมตของผู้ผลิตรายเดียว ติดตั้งส่วนขยายซ้ำอีกฝั่ง และยอมรับว่ารีโมตต้องน่าเชื่อถือพอที่จะรันมันได้

Mutagen ตัวนี้แหละที่จับโมเดลถูก: มีสำเนาจริงทั้งสองฝั่ง ซิงก์เดลตาต่อเนื่อง แก้บนเครื่อง รันบนรีโมต มันเป็นซอฟต์แวร์ที่ดีและสมกับชื่อเสียงที่ได้มา แต่แรงเสียดทานที่ผู้คนรายงานนั้นสอดคล้องกัน และมันมาจากรูปทรง ไม่ใช่คุณภาพ: มันคือ เดมอนและตัวจัดการเซสชัน ไม่ใช่คำสั่ง

รูปทรงแบบนั้นมีน้ำหนัก มีเดมอนที่อยู่ประจำบนเครื่องคุณ บวกกับเอเจนต์หนึ่งตัวต่อหนึ่งเอนด์พอยต์ มีชีวิตอยู่ไม่ว่าคุณจะทำงานอยู่หรือไม่ และในตัวติดตามปัญหาก็มีรายงานยาวเหยียดว่ามันเผา CPU ในช่วงที่ไฟล์ไม่ได้เปลี่ยนอะไรเลย บนต้นไม้ขนาดใหญ่ และภายใต้ WSL มันมีน้ำหนักบนดิสก์ด้วย: ในเวิร์กโฟลว์ Docker ที่มันถูกสร้างมาเพื่อรองรับ โปรเจกต์ของคุณมีอยู่สองชุด และเอกสารของ DDEV เองก็บอกให้คุณคอยดูวอลุ่มที่ซ้ำซ้อนนั้น — เกิน 5 GB คือคำเตือน เกิน 10 GB คือขั้นวิกฤต

ต่อมาคือพื้นที่เชิงปฏิบัติการ คุณสร้างเซสชัน ไล่ดูรายการ เรียนรู้ mutagen sync terminate — แล้วก็เรียนรู้ว่ามันค้างได้ระหว่างจัดเตรียมไฟล์ โดยสูตรพื้นบ้านที่วนอยู่ในเธรดปัญหาคือให้หยุดเดมอน ลบไฟล์ล็อก แล้วเริ่มใหม่ โหมดจัดการความขัดแย้งที่ปลอดภัยโดยดีฟอลต์ของมันจะหยุดรอมนุษย์ ซึ่งเป็นการตัดสินใจที่ถูก แต่ก็แปลว่าเซสชันอาจเลิกลู่เข้าหากันอย่างเงียบ ๆ ขณะที่คุณคิดว่าทุกอย่างเรียบร้อย มีคำขอค้างมานานเพียงเพื่อให้มีวิธีที่เชื่อถือได้ในการ รู้ ว่าเซสชันพังหรือเปล่า บนต้นไม้ใหญ่ การสแกนรอบแรกกินเวลาเป็นนาทีก่อนที่อะไรจะเริ่มทำงาน สัญญาณที่ชัดที่สุดว่าพื้นที่สะสมไปมากแค่ไหนคือ DDEV ซึ่งพึ่ง Mutagen หนักมาก สุดท้ายต้องออกซับคอมมานด์วินิจฉัยทั้งชุด — ddev utility mutagen-diagnose — เพื่อบอกคุณว่าทำไมการซิงก์ของคุณถึงไม่มีความสุข

แล้วพื้นก็ขยับ Docker ซื้อ Mutagen ไปในปี 2023 Mutagen Compose ถูกประกาศเลิกใช้ตรง ๆ — เอกสารบอกไว้ชัด: "Mutagen Compose is now deprecated with the release of v0.18.0." ส่วนตัว Mutagen เองไม่ได้ออกรีลีสที่ติดแท็กเลยนับตั้งแต่ v0.18.1 เมื่อกุมภาพันธ์ 2025 รีโพยังได้รับคอมมิตอัปเดต dependency เป็นครั้งคราว มันจึงยังไม่ตาย แต่ก็ไม่ได้ขยับ

ทั้งหมดนี้ไม่ใช่เรื่องอื้อฉาว และผมอยากยุติธรรมกับมัน: น้ำหนักที่ว่านั้นคือราคาที่เอนจินซิงก์อเนกประสงค์ต้องจ่าย เมื่อมันต้องรองรับเอนด์พอยต์ทั้งแบบโลคัล SSH และ Docker บนสี่ระบบปฏิบัติการ ภายใต้ตัวออร์เคสเตรต ผมแค่ไม่ต้องการอะไรในนั้นเลย ผมต้องการแค่ไดเรกทอรีหนึ่งกับพาธหนึ่ง

ผมจึงออกไปหาสิ่งที่เป็นแค่นั้น แล้วก็ไม่มี มี Unison ที่คุณต้องเรียน CLI ของมันใหม่ทุกครั้งที่แตะ มี Syncthing ที่สร้างขึ้นรอบ ๆ อุปกรณ์และโฟลเดอร์ที่แชร์กัน มากกว่ารอบต้นไม้ซอร์สโค้ดของนักพัฒนาคนหนึ่ง ถัดจากนั้นก็เป็นหางยาว ๆ ของโปรเจกต์ GitHub ที่ทำค้างไว้ ไม่มีอะไรที่เป็นแค่: ไดเรกทอรีนี้ พาธนั้น ผ่าน SSH เดี๋ยวนี้ เคารพ .gitignore ของผม — แล้วก็หลีกทางไป

ผมจึงเขียนมันเอง


ผมต้องการอะไรจากมัน

รายการข้อกำหนดสั้นมาก และทุกบรรทัดมาจากสิ่งที่เคยกวนใจผม:

  • หนึ่งคำสั่ง ไม่ใช่เซสชัน ไม่มีเดมอนให้ต้องคอยดูแล ไม่มีการเต้น status / terminate / ไฟล์ล็อก มันรันตราบที่คุณอยากให้รัน และ ctrl+c แปลว่าหยุด
  • ไม่มีอะไรอยู่ประจำ ตอนที่ผมไม่ได้ซิงก์ ก็ไม่มีโพรเซสอยู่เลย ไม่มีอะไรอุ่นแคช ไม่มีอะไรสแกนต้นไม้ ไม่มีอะไรให้ต้องสงสัยใน Activity Monitor ตอนตีสอง
  • ไม่มีไฟล์คอนฟิก สองพาธนั่นแหละคืออาร์กิวเมนต์ ไม่มีอะไรต้องคอมมิตเข้ารีโพ ไม่มีอะไรต้องจำ ไม่มีอะไรให้ล้าสมัย
  • .gitignore คือกฎหมาย รีโพของคุณประกาศไว้แล้วว่าอะไรไม่ใช่ซอร์ส target/, node_modules/, .venv/ — ไดเรกทอรีที่ทำให้เครื่องมือซิงก์ไร้เดียงสาทุกตัวคลาน — ไม่ควรเข้ามาอยู่ในแมนิเฟสต์ตั้งแต่แรก ไม่ว่าทิศทางไหน
  • ห้ามทำข้อมูลหาย เครื่องมือซิงก์ที่ลบสิ่งที่คุณตั้งใจจะเก็บไว้ ย่อมแย่กว่าการไม่มีเครื่องมือซิงก์เลย เรื่องอื่นต่อรองได้ทั้งหมด เรื่องนี้ไม่ได้
  • ถูกเงินบนต้นไม้ที่ซิงก์ไว้แล้ว การรันครั้งที่สองควรมีต้นทุนเท่ากับการเดินสำรวจไดเรกทอรี ไม่ใช่การแฮชใหม่ทั้งหมด
  • แค่ SSH กุญแจของคุณ ~/.ssh/config ของคุณ ProxyJump ของคุณ ไม่มีพื้นที่การยืนยันตัวตนใหม่ ไม่มีพอร์ตใหม่ให้ต้องเปิด

อ่านรายการนั้นอีกครั้งแล้วสังเกตว่าอะไรไม่อยู่ในนั้น: คอนเทนเนอร์ ออร์เคสเตรชัน การฟอร์เวิร์ดเครือข่าย Windows GUI สคีมาคอนฟิก ระบบปลั๊กอิน ทุกข้อล้วนเป็นสิ่งที่สมเหตุสมผลที่จะอยากได้ และทุกข้อก็คือเหตุผลที่เครื่องมือซิงก์งอกเดมอนออกมา การตัดมันออกไม่ใช่ความถ่อมตัว — มันคือทั้งหมดของการออกแบบ เครื่องมือยังเล็กอยู่ได้เพราะปัญหาที่มันยอมรับนั้นเล็ก

นั่นคือ Synx: ไบนารี Rust โอเพนซอร์สตัวเดียว — วันนี้คือเวอร์ชัน 0.1.2 — ที่มิเรอร์ไดเรกทอรีบนเครื่องไปยังพาธรีโมตผ่าน SSH และทำให้ทั้งสองเดินไปพร้อมกันขณะที่คุณทำงาน หนึ่งโพรเซส รันเฉพาะตอนที่คุณกำลังทำงาน ทำงานเดียว มันยังเป็นช่วงต้น และมันก็ทำงานที่ผมต้องการให้มันทำในทุก ๆ วันได้แล้ว


Synx คืออะไร

คุณรันมันบนเครื่องของคุณและชี้ไปยังเป้าหมายรีโมต:

synx ./src dev@beefy:/srv/app/src

นั่นคืออินเทอร์เฟซทั้งหมด ไบนารีตัวเดียวกันรันบนทั้งสองปลาย — บนเครื่องคือไคลเอนต์ บนรีโมตมันรันในโหมด --agent ที่ซ่อนอยู่ ซึ่งไคลเอนต์เปิดให้คุณผ่าน SSH ไม่มีบริการให้ติดตั้ง ไม่มีอะไรต้องลงทะเบียน ไม่มี YAML

มันทำการซิงก์เริ่มต้นเพื่อปรับให้ตรงกัน แล้วเข้าสู่โหมดเฝ้าดูแบบสด ที่ทุกการเปลี่ยนแปลงในด้านใดด้านหนึ่งจะส่งถึงอีกด้านภายในไม่กี่ร้อยมิลลิวินาที:

synx  /Users/dk/proj  ◀─▶  dev@beefy:/srv/proj
✓ connected
• manifests:  local 1243  •  remote 1180 (47 ignored)
• plan: push 78 files (4.2 MiB) 6 dirs 0 links  •  pull 14 entries
✓ initial sync: 4.2 MiB sent, 312 KiB received in 1.4s
• watching for changes — ctrl+c to stop
  → src/main.rs  3.1 KiB
  ← README.md   824 B

มันรันบน macOS และ Linux ทรานสปอร์ตคือ SSH ล้วน ๆ — คีย์ของคุณ เอเจนต์ของคุณ ~/.ssh/config ของคุณ ProxyJump ของคุณ ทั้งหมด Synx ไม่ได้คิดค้นกลไกยืนยันตัวตนใหม่ มันยืมตัวที่คุณเชื่อถืออยู่แล้วมาใช้


โหมดการซิงก์และการจัดการความขัดแย้ง

ทิศทางคือแฟล็กเดียว ค่าเริ่มต้นเป็นสองทิศทาง

โหมด ทิศทาง กฎความขัดแย้ง การซิงก์เริ่มต้น
push เครื่อง → รีโมต เครื่องชนะเสมอ ส่งเฉพาะไฟล์ที่มีแค่บนเครื่องหรือต่างกัน
pull รีโมต → เครื่อง รีโมตชนะเสมอ ดึงเฉพาะไฟล์ที่มีแค่บนรีโมตหรือต่างกัน
both (ค่าเริ่มต้น) สองทิศทาง mtime ใหม่กว่าชนะ ผสาน ไม่มีการลบ
# สองทิศทาง (ค่าเริ่มต้น)
synx ./src dev@host:/srv/app/src

# push ทิศทางเดียว
synx ./build host:/var/www --mode push

# pull ทิศทางเดียว
synx ./nginx host:/etc/nginx --mode pull

ผมจะพูดตรง ๆ เรื่องโมเดลความขัดแย้งเพราะมันสำคัญ: ในโหมด both เมื่อไฟล์เดียวกันเปลี่ยนทั้งสองด้าน เวลาที่แก้ไขล่าสุดกว่าจะชนะ แค่นั้น ไม่มีการผสานสามทางที่รู้บรรพบุรุษร่วม ไม่มีเครื่องหมายความขัดแย้ง สิ่งนี้เชื่อถือได้ตราบเท่าที่นาฬิกาของทั้งสองเครื่องปกติดี — ดังนั้นถ้าคุณเห็นไฟล์เด้งไปเด้งมา สิ่งแรกที่ต้องตรวจคือความคลาดเคลื่อนของนาฬิกา (NTP แก้ได้) และถ้าคุณเชื่อนาฬิกาไม่ได้ ก็เลือกทิศทาง push หรือ pull ให้ชัดเจนไปเลย

อีกหนึ่งการตัดสินใจที่จงใจ: การซิงก์เริ่มต้นไม่เคยลบอะไรเลย ถ้าคุณชี้ Synx ไปยังพาธรีโมตที่ล้าสมัย มันจะไม่ล้างข้อมูล — มันผสาน การลบจะแพร่กระจายก็ต่อเมื่อ Synx ทำงานสดและกำลังเฝ้าดู และต่อเมื่อมันมี baseline เท่านั้น — สแน็ปช็อตที่บันทึกไว้ว่าทั้งสองด้านตกลงกันครั้งล่าสุดเป็นอย่างไร baseline อยู่ในไดเรกทอรีแคชของคุณ และทำให้ Synx แยกแยะได้ระหว่าง "ไฟล์นี้ถูกลบ" กับ "ไฟล์นี้ไม่เคยมีอยู่บนอีกด้านเลย" การรันใหม่ที่ไม่มี baseline จะเก็บทุกอย่างไว้ การลบจะเริ่มแพร่กระจายตั้งแต่การซิงก์ครั้งที่สองเป็นต้นไป ความไม่สมมาตรนี้ตั้งใจให้เป็นแบบนั้น การสูญข้อมูลเพราะเครื่องมือซิงก์ที่กระตือรือร้นเกินไปคือโหมดความล้มเหลวเดียวที่ผมปฏิเสธจะปล่อยออกไป


กฎการละเว้นคือผู้มีอำนาจสูงสุด

Synx โหลด .gitignore ทุกไฟล์ ใต้รากการซิงก์ของคุณ — แบบซ้อนกันที่ความลึกใดก็ตาม — บวกกับ .synxignore ที่เป็นทางเลือกซึ่งมีไวยากรณ์เหมือนกันทุกประการ อะไรก็ตามที่ตรงกันจะไม่ถูกซิงก์เลย ไม่ว่าทิศทางใด นี่ไม่ใช่ฟิลเตอร์แบบ "พยายามให้ดีที่สุด" ที่ใช้ที่ต้นทาง มันถูกบังคับใช้ที่สามจุด:

  1. การเดินสำรวจเริ่มต้น — ไฟล์ที่ถูกละเว้นจะไม่เข้าไปอยู่ในแมนิเฟสต์ตั้งแต่แรก
  2. แมนิเฟสต์ฝั่งรีโมต — ไฟล์ที่รีโมตรายงานมาและตรงกับกฎการละเว้น บนเครื่องคุณ จะถูกกรองออกก่อนที่แผน diff จะทำงาน ดังนั้น target/ หรือ node_modules/ ที่บังเอิญมีอยู่บนรีโมตจะไม่ถูกดึงลงมาเลย
  3. เหตุการณ์แบบสด — ทั้งการนำเข้ามาใช้และการแจ้งเตือนขาออกต่างก็ข้ามพาธที่ถูกละเว้น

สิ่งหนึ่งที่ทำให้คนแปลกใจ: ไฟล์ที่ขึ้นต้นด้วยจุด (dotfiles) ไม่ใช่อะไรพิเศษ .env .vscode/ .git/ — ทั้งหมดถูกซิงก์เหมือนอย่างอื่นเว้นแต่คุณจะกันมันออก ถ้าคุณไม่อยากให้ไดเรกทอรี .git/ ถูกมิเรอร์ ก็บอกไปเลย:

echo '/.git' >> .synxignore

พูดถึง .git/ — Synx ระมัดระวังกับมัน Git มองไดเรกทอรีนั้นเป็นสถานะแบบทรานแซกชัน และการเปลี่ยนชื่อแบบอะตอมมิกของอ็อบเจกต์ที่เขียนค้างหรือไฟล์ล็อกไปยังพีร์กลางคันของคอมมิตจะทำให้รีโพเสียหาย Synx จึงเฝ้าดูเครื่องหมายปฏิบัติการที่กำลังดำเนินอยู่ของ git (index.lock และพวกพ้อง) และ หยุดการซิงก์พาธของ .git/ ชั่วคราว ขณะที่ปฏิบัติการ git กำลังทำงานอยู่ แล้วเล่นการเปลี่ยนแปลงที่หน่วงไว้ซ้ำเมื่อ git เสร็จ working tree ของคุณซิงก์ต่อไปตลอดเวลา มีแค่ .git/ ที่รอ ถ้าคุณกำลังจะมิเรอร์รีโพที่มีชีวิต คุณต้องการสิ่งนี้

ในทิศทางกลับกันก็มีการป้องกันเช่นกัน ถ้า .git/ บนเครื่องคุณเป็นของเหลือจากปฏิบัติการที่ถูกขัดจังหวะ และฝั่งรีโมตไม่มี .git/ เลย Synx จะมิเรอร์การลบนั้น ก็ต่อเมื่อ baseline พิสูจน์ได้ว่า .git/ เป็นส่วนหนึ่งของสถานะล่าสุดที่ทั้งสองฝั่งตกลงกันไว้ เท่านั้น หากไม่มีหลักฐานนั้น .git/ ของคุณก็เป็นเพียงข้อมูลที่ยังไม่เคยซิงก์ — Synx จะเก็บมันไว้แล้วส่งขึ้นไปยังรีโมตแทน


มันทำงานอย่างไรภายใน

┌─ local (client) ──────────────┐         ┌─ remote (agent) ─────────────┐
│  watcher (notify)             │   ssh   │  watcher (notify)            │
│  parallel walker (blake3)     │ ◀─────▶ │  parallel walker (blake3)    │
│  persistent hash cache        │  stdio  │  persistent hash cache       │
│  diff plan + executor         │ postcard│  message dispatcher          │
└───────────────────────────────┘  + zstd └──────────────────────────────┘

มีบางส่วนที่ควรอธิบาย เพราะความเร็วมาจากตรงนั้น

การแฮชแบบขนานพร้อมแคชถาวร ทั้งสองด้านเดินสำรวจต้นไม้ของตนแบบขนาน (ผ่านตัวเดินสำรวจแบบขนานของเครต ignore) และแฮชทุกไฟล์ด้วย blake3 ไฟล์ ignore ที่ซ้อนกันถูกค้นพบภายในการเดินสำรวจครั้งเดียวกันนั้น ไม่ใช่ในรอบสำรวจแยกต่างหาก การเริ่มต้นจึงเสียการเดินสำรวจต้นไม้เพียงรอบเดียวแทนที่จะเป็นสองรอบ — และตัวเฝ้าดูถูกเปิดใช้ ก่อน ที่การเดินสำรวจจะเริ่ม สิ่งที่คุณบันทึกระหว่างที่มันยังสแกนอยู่จึงเข้าคิวไว้แทนที่จะหายไป แฮชเข้าไปอยู่ในแคชถาวรในไดเรกทอรีแคชของแพลตฟอร์มคุณ (~/.cache/synx/ บน Linux, ~/Library/Caches/synx/ บน macOS) ทำดัชนีด้วย (path, size, mtime) การรัน Synx ซ้ำบนรีโพที่ไม่เปลี่ยนจะข้ามการแฮชซ้ำทั้งหมด — การซิงก์ครั้งที่สองของต้นไม้แสนไฟล์ถูกจำกัดด้วยการเดินสำรวจไดเรกทอรีเพียงอย่างเดียว ซึ่งประมาณหนึ่งวินาที การอ่านแคชเป็นแบบ immutable และผลลัพธ์ของตัวเดินสำรวจถูกรวมเป็นชุดต่อ worker จึงไม่มีล็อกส่วนกลางในลูปที่ร้อน และแคชที่ไม่มีการเปลี่ยนแปลงก็ไม่ถูกเขียนกลับลงดิสก์เลย

การถ่ายโอนแบบเดลตาสำหรับไฟล์ใหญ่ ไฟล์เล็กหรือไฟล์ที่เปลี่ยนทั้งก้อนจะเดินทางผ่านสายแบบเต็มไฟล์ แต่สำหรับไฟล์ ขนาดระหว่าง 256 KiB ถึง 256 MiB ที่ฝั่งพีร์มีเวอร์ชันอื่นอยู่แล้ว Synx ทำเดลตาสไตล์ rsync มันใช้ fast_rsync ซึ่งเป็นพอร์ตของ librsync ที่เร่งด้วย SIMD: ฝั่งรับคำนวณลายเซ็นของไฟล์ที่มีอยู่แล้ว ฝั่งส่ง diff เทียบกับลายเซ็นนั้น และมีเพียงบล็อกที่เปลี่ยนเท่านั้นที่เดินทาง เนื่องจากการแฮชบล็อกภายในของ librsync เก่ากว่าแฮชเชิงเข้ารหัสสมัยใหม่ Synx จึงตรวจสอบผลของทุกเดลตาที่นำมาใช้เทียบกับแฮช blake3 ใหม่ก่อนคอมมิต สายส่งจะไม่มีโอกาสโกหกคุณ

การบีบอัดและการแบ่งชังก์ ข้อความเป็น postcard แบบนำหน้าด้วยความยาว (รูปแบบไบนารีกระชับที่ได้รับการดูแลอย่างต่อเนื่อง) และถูกบีบอัดด้วย zstd เมื่อการบีบอัดประหยัดพื้นที่จริง รูปแบบที่ถูกบีบอัดมาแล้ว — ไฟล์บีบอัด สื่อ แพ็กเกจ — จะข้าม zstd ตามนามสกุลไฟล์ เพื่อไม่ให้เปลือง CPU ไปกับการพิสูจน์ว่ามันเล็กลงไม่ได้ ไฟล์ที่ใหญ่กว่า 16 MiB จะถูกสตรีมเป็นชังก์ขนาด 4 MiB ลงในไฟล์ชั่วคราว แล้วเปลี่ยนชื่อแบบอะตอมมิกเข้าที่โดยรักษาโหมดและ mtime เดิม — ดังนั้นการแครชกลางการถ่ายโอนจะไม่มีวันทิ้งไฟล์ที่เขียนค้างไว้ที่พาธจริง

การกดเสียงสะท้อนตามสถานะ นี่คือส่วนที่ละเอียดอ่อนของการซิงก์สองทิศทางใด ๆ เมื่อ Synx นำการเปลี่ยนแปลงขาเข้ามาใช้ ตัวเฝ้าดูบนเครื่องของคุณกำลังจะทำงานสำหรับพาธเดียวกันนั้น — และถ้าคุณส่งมันกลับไปอย่างซื่อ ๆ คุณก็ได้เสียงสะท้อนไม่รู้จบ ทางแก้แบบขี้เกียจคือหน้าต่างเวลา ("ละเว้นเหตุการณ์ในอีก N มิลลิวินาทีข้างหน้า") ซึ่งก็ทิ้งการแก้ไข ที่ถูกต้อง ที่ผู้ใช้ทำในช่วงนั้นไปด้วย Synx กลับบันทึกสถานะผลลัพธ์บนดิสก์แทน (mtime หรือ "ถูกลบ") และเมื่อตัวเฝ้าดูทำงาน มันเปรียบเทียบไฟล์ ปัจจุบัน กับสิ่งที่บันทึกไว้ มีเพียงค่าที่ตรงกันเท่านั้นที่ถือว่าเป็นเสียงสะท้อนและถูกทิ้ง ถ้าคุณแก้ไขไฟล์ในระหว่างนั้น เหตุการณ์ก็ไหลผ่านตามปกติ ไม่มีหน้าต่างตาบอดที่การกดแป้นของคุณถูกกลืนหายไป

การใช้การเชื่อมต่อซ้ำ SSH ทำงานด้วย ControlMaster auto และระยะคงอยู่สั้น ๆ ดังนั้นการเรียก Synx หลายครั้งไปยังโฮสต์เดียวกันจึงใช้การเชื่อมต่อ TCP เดียวกันร่วมกันแทนที่จะเจรจาใหม่


ลองใช้ใน 5 นาที

คุณต้องมี Synx บนทั้งสองปลาย — เครื่องของคุณและรีโมต ตัวติดตั้งแบบบรรทัดเดียวคือทางที่เร็วที่สุดบนแต่ละเครื่อง:

# Linux และ macOS, x86_64 + ARM64
curl -fsSL https://raw.githubusercontent.com/Muvon/synx/master/install.sh | sh

# หรือจาก crates.io
cargo install synx

ถ้าคุณมีบิลด์ release บนเครื่องอยู่แล้ว ก็แค่คัดลอกมันไป:

scp target/release/synx user@host:~/.local/bin/synx
ssh user@host 'chmod +x ~/.local/bin/synx'

จากนั้นเริ่มเซสชัน:

# ซิงก์สองทิศทาง แบบสด
synx ./project dev@host:/srv/project

# ดูแผน ไม่เปลี่ยนอะไร
synx ./project dev@host:/srv/project --dry-run

# ซิงก์เริ่มต้นอย่างเดียว แล้วออก
synx ./project dev@host:/srv/project --once

แฟล็กบางตัวที่คุณจะหยิบมาใช้:

# พอร์ต SSH ที่ไม่มาตรฐาน (หรืออาร์กิวเมนต์ ssh เพิ่มเติมใด ๆ)
synx ./code host:/work --ssh-opts "-p 2222 -i ~/.ssh/devkey"

# synx ไม่อยู่ใน PATH บนรีโมต
synx ./code host:/work --remote-synx ~/.local/bin/synx

# ข้ามการบีบอัด (เร็วกว่าบน LAN ที่เร็วซึ่งมี blob ที่บีบอัดไม่ได้)
synx ./code host:/work --no-compress

# ล็อกมากขึ้น
synx ./code host:/work -v     # debug
synx ./code host:/work -vv    # trace

ถ้ารีโมตหาไบนารีไม่เจอ คุณจะได้ synx: command not found จากเชลล์ล็อกอิน — นั่นคือกรณีของ --remote-synx ไม่ใช่บั๊ก และทั้งสองปลายต้องรันโปรโตคอลเวอร์ชันเดียวกัน 0.1.x พูดโปรโตคอล v1 และเข้ากันไม่ได้ในระดับสายส่งกับสิ่งที่จะออกมาในอนาคต ดังนั้นจงอัปเกรดทั้งสองด้านพร้อมกัน


สิ่งที่มันยังทำไม่ได้

Synx คือ 0.1.2 ผมขอบอกคุณถึงขอบมุมของมันเองดีกว่าปล่อยให้คุณไปเจอเอง

  • ไม่มีโหมดเดมอน มันรันแบบ foreground ส่งไป background ด้วย & หรืออยู่ใน tmux/screen ส่วน synx status / synx stop ที่เหมาะสมอยู่ในรายการ
  • ไม่มีการผสานเนื้อหาแบบสามทาง การลบมี baseline หนุนหลัง แต่ความขัดแย้งของ เนื้อหา ไฟล์ตัดสินด้วย mtime ที่ใหม่กว่า ไม่รู้บรรพบุรุษร่วม ต้องการนาฬิกาที่ถูกต้อง
  • แคชแฮชใช้คีย์ (size, mtime) ไฟล์ที่ถูกเขียนทับที่เดิมด้วยขนาดและเวลาประทับเท่ากันจะไม่ถูกแฮชใหม่ นี่คือฮิวริสติกเดียวกับที่ git ใช้ และในทางปฏิบัติมันถูกต้อง
  • รีสตาร์ตเพื่อรับกฎการละเว้นใหม่ เปลี่ยน .gitignore กลางเซสชัน แล้ว Synx จะไม่รู้จนกว่าคุณจะรีสตาร์ตมัน
  • เฉพาะ macOS และ Linux ไม่รองรับ Windows

ไม่มีข้อใดเป็นอุปสรรคใหญ่สำหรับการใช้งานหลัก — แก้ตรงนี้ รันตรงนั้น — ซึ่งเป็นสิ่งที่ผมสร้างมันขึ้นมาเพื่อสิ่งนั้นพอดี และเป็นสิ่งที่มันทำได้ดีในวันนี้


โอเพนซอร์ส

Synx อยู่บน GitHub ภายใต้ Apache-2.0 มันเป็น Rust เพราะการซิงก์ไฟล์ต้องการความถูกต้องในเวลาคอมไพล์ ความหน่วงที่คาดเดาได้ และไม่มีการหยุดของ GC กลางการถ่ายโอน — และเพราะไบนารีสแตติกตัวเดียวที่คุณ scp ขึ้นเซิร์ฟเวอร์ได้คือรูปทรงที่ถูกต้องสำหรับเครื่องมือแบบนี้

มันสร้างโดยทีมเดียวกับที่อยู่เบื้องหลังเครื่องมือนักพัฒนาตัวอื่น ๆ ของเรา ถ้าคุณอยากรู้ว่าทำไมเราถึงปล่อยยูทิลิตีโอเพนซอร์สเล็ก ๆ คม ๆ ออกมาเรื่อย ๆ แทนที่จะเป็นแพลตฟอร์มใหญ่ตัวเดียว นั่นเป็นบทสนทนาที่ยาวกว่านี้ Synx คือตัวล่าสุดในนั้น และเป็นตัวที่ผมเองใช้มากที่สุด

ถ้ามันมีประโยชน์กับคุณ โค้ดก็อยู่ตรงนั้น ถ้ามันพัง ตัวติดตามปัญหาก็อยู่ตรงนั้นเช่นกัน — รายงานบั๊กจากการตั้งค่าการพัฒนาแบบรีโมตจริง ๆ คือทางที่เร็วที่สุดในการทำให้ 0.2 ดีกว่า 0.1

— Vladimir

Synx เป็นโอเพนซอร์สภายใต้ Apache-2.0 ดาวน์โหลดมัน อ่านซอร์ส หรือแจ้งปัญหาได้ที่ github.com/Muvon/synx