كيف تهزم التنين: أعد كتابة برنامجك على Golang

لقد حدث أن برنامجك مكتوب بلغة نصية - على سبيل المثال ، في روبي - وتحتاج إلى إعادة كتابته بلغة غولانغ.


سؤال معقول: لماذا قد تحتاج على الإطلاق إلى إعادة كتابة برنامج تمت كتابته بالفعل ويعمل بشكل جيد؟



أولاً ، دعنا نقول أن البرنامج يرتبط بنظام بيئي محدد - في حالتنا ، هذه هي Docker و Kubernetes. البنية التحتية الكاملة لهذه المشاريع مكتوبة في Golang. هذا يفتح الوصول إلى المكتبات التي تستخدم Docker ، Kubernetes وغيرها. من وجهة نظر دعم وتطوير وصقل برنامجك ، من الأفضل استخدام نفس البنية الأساسية التي تستخدمها المنتجات الرئيسية. في هذه الحالة ، ستكون جميع الميزات الجديدة متاحة على الفور ولن تضطر إلى تنفيذها مرة أخرى بلغة أخرى. فقط هذا الشرط في وضعنا المحدد كان كافيا لاتخاذ قرار بشأن الحاجة إلى تغيير اللغة من حيث المبدأ ، ونوع اللغة التي ينبغي أن تكون. هناك ، مع ذلك ، مزايا أخرى ...


ثانيا ، سهولة تثبيت التطبيقات على Golang. لا تحتاج إلى تثبيت Rvm ، أو Ruby ، ​​أو مجموعة من الأحجار الكريمة ، وما إلى ذلك في النظام. تحتاج إلى تنزيل ملف ثنائي ثابت واحد واستخدامه.


ثالثا ، سرعة البرامج في Golang أعلى. هذه ليست زيادة نظامية كبيرة في السرعة ، والتي يتم الحصول عليها باستخدام البنية الصحيحة والخوارزميات في أي لغة. ولكن هذه الزيادة تبدو محسوسة عند بدء تشغيل البرنامج من وحدة التحكم. على سبيل المثال ، --help في روبي يمكن أن تنجح في 0.8 ثانية ، وعلى Golang - 0.02 ثانية. إنه يحسن بشكل ملحوظ تجربة المستخدم في استخدام البرنامج.


ملحوظة : كما كان يمكن للقراء العاديين في مدونتنا أن يتخيلوا ، تستند المقالة إلى تجربة إعادة كتابة منتج dapp لدينا ، والذي أصبح الآن - ليس رسميًا (حتى الآن) المعروف رسميًا باسم werf . انظر نهاية المقال لمزيد من التفاصيل حول هذا الموضوع.


جيد: يمكنك فقط التقاط وكتابة رمز جديد معزول تمامًا عن رمز البرنامج النصي القديم. ولكن على الفور تظهر بعض الصعوبات والقيود على الموارد والوقت المخصص للتنمية:


  • يحتاج الإصدار الحالي من البرنامج في روبي باستمرار إلى التحسينات والتصحيحات:
    • تحدث الأخطاء عند استخدامها ويجب إصلاحها على الفور ؛
    • لا يمكنك تجميد إضافة ميزات جديدة لمدة ستة أشهر ، لأن غالبًا ما تكون هذه الميزات مطلوبة من قِبل العملاء / المستخدمين.
  • الحفاظ على 2 من الأكواد البرمجية في نفس الوقت أمر صعب ومكلف:
    • هناك عدد قليل من الفرق من 2-3 أشخاص ، بالنظر إلى وجود مشاريع أخرى إلى جانب برنامج روبي هذا.
  • مقدمة الإصدار الجديد:
    • يجب ألا يكون هناك تدهور كبير في الوظيفة ؛
    • من الناحية المثالية ، يجب أن يكون هذا سلسًا وسلسًا.

مطلوب عملية ترقية مستمرة. ولكن كيف يمكنني القيام بذلك إذا تم تطوير إصدار Golang كبرنامج مستقل؟


نكتب بلغتين في وقت واحد


ولكن ماذا لو قمت بنقل المكونات إلى Golang من الأسفل إلى الأعلى؟ نبدأ بأشياء منخفضة المستوى ، ثم ننطلق من التجريدات.


تخيل أن البرنامج يتكون من المكونات التالية:


 lib/ config.rb build/ image.rb git_repo/ base.rb local.rb remote.rb docker_registry.rb builder/ base.rb shell.rb ansible.rb stage/ base.rb from.rb before_install.rb git.rb install.rb before_setup.rb setup.rb deploy/ kubernetes/ client.rb manager/ base.rb job.rb deployment.rb pod.rb 

مكون المنفذ مع الميزات


حالة بسيطة. نحن نأخذ مكونًا حاليًا معزول تمامًا عن الباقي - على سبيل المثال ، config ( lib/config.rb ). في هذا المكون ، يتم تعريف وظيفة Config::parse فقط ، والتي تأخذ المسار إلى التهيئة ، وتقرأها ، وتنتج بنية مملوءة. سيكون هناك ثنائي منفصل في config Golang وتكوين الحزمة المقابلة سيكون مسؤولاً عن تنفيذه:


 cmd/ config/ main.go pkg/ config/ config.go 

يتلقى ثنائي Golang الوسائط من ملف JSON ويقوم بإخراج النتيجة إلى ملف JSON.


 config -args-from-file args.json -res-to-file res.json 

من config أن config يمكن أن يخرج الرسائل إلى stdout / stderr (في برنامج Ruby الخاص بنا ، ينتقل الإخراج دائمًا إلى stdout / stderr ، لذلك لا يتم تحديد هذه الميزة).


استدعاء ثنائي config يكافئ استدعاء بعض وظائف مكون config . تشير الوسائط من خلال ملف args.json إلى اسم الوظيفة والمعلمات الخاصة بها. في الإخراج من خلال ملف res.json ، res.json على نتيجة الدالة. إذا كان يجب أن تقوم الدالة بإرجاع كائن من فئة ما ، فسيتم إرجاع بيانات كائن هذه الفئة في شكل JSON التسلسلي.


على سبيل المثال ، للاتصال بوظيفة Config::parse args.json ، حدد args.json التالية:


 { "command": "Parse", "configPath": "path-to-config.yaml" } 

res.json النتيجة في res.json :


 { "config": { "Images": [{"Name": "nginx"}, {"Name": "rails"}], "From": "ubuntu:16.04" }, } 

في مجال config ، نحصل على حالة كائن Config::Config المتسلسل في JSON. من هذه الحالة ، على المتصل في روبي ، تحتاج إلى إنشاء كائن Config::Config .


في حالة حدوث خطأ ، يمكن للثنائي إرجاع JSON التالي:


 { "error": "no such file path-to-config.yaml" } 

يجب أن يتعامل المتصل مع حقل error .


استدعاء Golang من روبي


من جانب روبي ، نقوم بتحويل Config::parse(config_path) إلى غلاف يستدعي config ، ويحصل على النتيجة ، Config::parse(config_path) جميع الأخطاء المحتملة. فيما يلي مثال لرمز الكود الكاذب روبي مع التبسيط:


 module Config def parse(config_path) call_id = get_random_number args_file = "#{get_tmp_dir}/args.#{call_id}.json" res_file = "#{get_tmp_dir}/res.#{call_id}.json" args_file.write(JSON.dump( "command" => "Parse", "configPath" => config_path, )) system("config -args-from-file #{args_file} -res-to-file #{res_file}") raise "config failed with unknown error" if $?.exitstatus != 0 res = JSON.load_file(res_file) raise ParseError, res["error"] if res["error"] return Config.new_from_state(res["config"]) end end 

قد يتعطل الثنائي برمز غير صفري وغير متوقع - وهذا وضع استثنائي. أو باستخدام الرموز المتوفرة - في هذه الحالة ، ننظر إلى ملف res.json لوجود حقول error res.json ونتيجة لذلك ، نرجع كائن Config::Config من حقل config المسلسل.


من وجهة نظر المستخدم ، لم تتغير وظيفة Config::Parse .


فئة مكون المنفذ


على سبيل المثال ، خذ التسلسل الهرمي للفئة lib/git_repo . هناك فئتان: GitRepo::Local و GitRepo::Remote . من المنطقي الجمع بين تنفيذها في git_repo واحد ثنائي ، وبالتالي ، git_repo package في Golang.


 cmd/ git_repo/ main.go pkg/ git_repo/ base.go local.go remote.go 

استدعاء ثنائي git_repo يناظر استدعاء بعض طريقة GitRepo::Local أو GitRepo::Remote . الكائن له حالة ويمكن أن يتغير بعد استدعاء الأسلوب. لذلك ، في الوسيطات ، نقوم بتمرير الحالة الحالية المتسلسلة في JSON. وعند الإخراج ، نحصل دائمًا على الحالة الجديدة للكائن - أيضًا في JSON.


على سبيل المثال ، للاتصال local_repo.commit_exists?(commit) ، نحدد args.json التالية:


 { "localGitRepo": { "name": "my_local_git_repo", "path": "path/to/git" }, "method": "IsCommitExists", "commit": "e43b1336d37478282693419e2c3f2d03a482c578" } 

الإخراج هو res.json :


 { "localGitRepo": { "name": "my_local_git_repo", "path": "path/to/git" }, "result": true, } 

في حقل localGitRepo ، يتم استلام حالة جديدة للكائن (والتي قد لا تتغير). يجب أن نضع هذه الحالة في روبي كائن local_git_repo الحالي على أي حال.


استدعاء Golang من روبي


من جانب روبي ، نحول كل طريقة من GitRepo::Base ، GitRepo::Local ، GitRepo::Remote إلى مغلفات تستدعي git_repo لدينا ، نحصل على النتيجة ، اضبط الحالة الجديدة للكائن من فئة GitRepo::Local أو GitRepo::Remote .


خلاف ذلك ، كل شيء يشبه استدعاء وظيفة بسيطة.


كيفية التعامل مع تعدد الأشكال والطبقات الأساسية


أسهل طريقة هي عدم دعم تعدد الأشكال من Golang. أي تأكد من أن المكالمات إلى git_repo الثنائية دائمًا ما git_repo توجيهها بشكل صريح إلى تطبيق معين (إذا localGitRepo تحديد localGitRepo في الوسيطات ، فقد جاءت المكالمة من كائن GitRepo::Local class ؛ إذا remoteGitRepo تحديد remoteGitRepo - ثم من GitRepo::Remote ) واحصل على نسخ كمية صغيرة من boilerplate- كود في كمد. بعد كل شيء ، على أي حال ، سيتم طرح هذا الرمز بمجرد اكتمال الانتقال إلى Golang.


كيفية تغيير حالة كائن آخر


هناك حالات عندما يستقبل كائن كائنًا آخر كمعلمة ويستدعي طريقة تغير حالة هذا الكائن الثاني ضمنيًا.


في هذه الحالة ، يجب عليك:


  1. عندما يتم استدعاء ثنائي ، بالإضافة إلى الحالة المتسلسلة للكائن الذي يتم استدعاء الطريقة إليه ، قم بنقل الحالة المتسلسلة لجميع كائنات المعلمات.
  2. بعد المكالمة ، قم بإعادة تعيين حالة الكائن الذي تم استدعاء الطريقة إليه ، وكذلك إعادة تعيين حالة جميع الكائنات التي تم تمريرها كمعلمات.

خلاف ذلك ، كل شيء مشابه.


ما هذا؟


نأخذ مكونًا ، منفذًا إلى Golang ، لإصدار نسخة جديدة.


في حالة نقل المكونات الأساسية بالفعل ونقل عنصر عالي المستوى يستخدمها ، يمكن لهذا المكون "قبول" هذه المكونات الأساسية . في هذه الحالة ، قد يتم بالفعل حذف الثنائيات الإضافية المقابلة باعتبارها غير ضرورية.


ويستمر هذا حتى نصل إلى الطبقة العليا ، التي تجمع بين جميع التجريدات الأساسية . هذا سيكمل المرحلة الأولى من النقل. الطبقة العليا هي CLI. لا يزال بإمكانه العيش على روبي لفترة من الوقت قبل أن يتحول إلى Golang بالكامل.


كيفية توزيع هذا الوحش؟


جيد: الآن لدينا نهج لمنفذ تدريجيا جميع المكونات. السؤال: كيفية توزيع مثل هذا البرنامج في لغتين؟


في حالة روبي ، لا يزال البرنامج مثبتًا كجوهرة. حالما يتعلق الأمر بالاتصال بالثنائي ، يمكن تنزيل هذه التبعية إلى عنوان URL محدد (وهو مضغوط) وتخزينه مؤقتًا محليًا في النظام (في مكان ما في ملفات الخدمة).


عندما نصدر إصدارًا جديدًا من برنامجنا بلغتين ، يجب علينا:


  1. جمع وتحميل جميع التبعيات الثنائية لاستضافة معينة.
  2. قم بإنشاء نسخة جديدة من Ruby Gem.

يتم تجميع الثنائيات لكل إصدار لاحق بشكل منفصل ، حتى لو لم يتغير أحد المكونات. يمكن للمرء أن يجعل إصدار منفصل لجميع الثنائيات التابعة. ثم لن يكون من الضروري جمع ثنائيات جديدة لكل إصدار جديد من البرنامج. لكن في حالتنا ، تابعنا من حقيقة أنه ليس لدينا وقت للقيام بشيء معقد للغاية وتحسين رمز الوقت ، لذلك قمنا ببساطة بجمع ثنائيات منفصلة لكل إصدار من البرنامج على حساب توفير الوقت ووقت التنزيل.


عيوب النهج


من الواضح ، يتم إنشاء الحمل العام لاستدعاء البرامج الخارجية باستمرار من خلال system / exec .


من الصعب تخزين أي بيانات عامة على مستوى Golang - بعد كل شيء ، يتم إنشاء جميع البيانات في Golang (على سبيل المثال ، متغيرات الحزمة) عندما يتم استدعاء طريقة وتموت بعد الانتهاء. يجب أن يؤخذ هذا دائمًا في الاعتبار. ومع ذلك ، لا يزال التخزين المؤقت ممكنًا على مستوى مثيل الفئة أو بتمرير المعلمات بشكل صريح إلى مكون خارجي.


يجب ألا ننسى نقل حالة الأشياء إلى Golang واستعادتها بشكل صحيح بعد المكالمة.


تشغل التبعيات الثنائية على Golang مساحة كبيرة . إنه شيء واحد عندما يكون هناك ثنائي واحد بحجم 30 ميغابايت - وهو برنامج على Golang. شيء آخر ، عندما قمت بنقل حوالي 10 مكونات ، يزن كل منها 30 ميغابايت ، نحصل على 300 ميغابايت من الملفات لكل إصدار . ولهذا السبب ، فإن المساحة الموجودة على الاستضافة الثنائية وعلى الجهاز المضيف ، حيث يعمل البرنامج ويتم تحديثه باستمرار ، تترك بسرعة. ومع ذلك ، فإن المشكلة ليست مهمة إذا قمت بحذف الإصدارات القديمة بشكل دوري.


لاحظ أيضًا أنه مع كل تحديث للبرنامج ، سيستغرق الأمر بعض الوقت لتنزيل التبعيات الثنائية.


مزايا النهج


على الرغم من جميع العيوب المذكورة ، فإن هذا النهج يسمح لك بتنظيم عملية مستمرة للانتقال إلى لغة أخرى والمتابعة مع فريق تطوير واحد.


الميزة الأهم هي القدرة على الحصول على تعليقات سريعة على الكود الجديد واختباره وتثبيته.


في هذه الحالة ، يمكنك ، من بين أشياء أخرى ، إضافة ميزات جديدة إلى البرنامج ، وإصلاح الخلل في الإصدار الحالي.


كيفية القيام بانقلاب نهائي على Golang


في الوقت الذي يتم فيه تحويل جميع المكونات الرئيسية إلى Golang وتم اختبارها بالفعل في الإنتاج ، يبقى فقط إعادة كتابة الواجهة العليا لبرنامجك (CLI) إلى Golang ورمي رمز Ruby القديم.


في هذه المرحلة ، يبقى حل مشكلات توافق CLI الجديدة مع القديم فقط.


الصيحة ، أيها الرفاق! لقد أصبحت الثورة حقيقة.


كيف نعيد كتابة dapp على Golang


Dapp عبارة عن أداة مساعدة تم تطويرها بواسطة Flant لتنظيم عملية CI / CD. وقد كتب في روبي لأسباب تاريخية:


  • خبرة واسعة في تطوير البرامج في روبي.
  • يستخدم الشيف (وصفات له مكتوبة في روبي).
  • القصور الذاتي ، ومقاومة استخدام لغة جديدة بالنسبة لنا لشيء خطير.

تم تطبيق النهج الموصوف في المقال لإعادة كتابة dapp على Golang. يوضح الرسم البياني أدناه التسلسل الزمني للصراع بين الخير (جولانج ، الأزرق) والشر (روبي ، الأحمر):



مقدار الكود في مشروع dapp / werf بلغة Ruby مقابل اللغات Golang على مدار الإصدارات


في الوقت الحالي ، يمكنك تنزيل إصدار ألفا 1.0 ، الذي لا يحتوي على روبي. قمنا أيضًا بإعادة تسمية dapp to werf ، ولكن هذه قصة أخرى ... انتظر الإصدار الكامل من werf 1.0 قريبًا!


كمزايا إضافية لهذه الهجرة وتوضيح التكامل مع النظام البيئي الشهير Kubernetes ، نلاحظ أن إعادة كتابة dapp على Golang أعطانا الفرصة لإنشاء مشروع آخر - kubedog . لذلك تمكنا من فصل الشفرة لتتبع موارد K8s في مشروع منفصل ، مما قد يكون مفيدًا ليس فقط في werf ، ولكن أيضًا في مشاريع أخرى. هناك حلول أخرى لنفس المهمة (انظر إعلاننا الأخير للحصول على التفاصيل) ، ولكن "التنافس" معهم (من حيث الشعبية) دون Go لأن أساسها سيكون بالكاد يصبح ممكنًا.


PS


اقرأ أيضًا في مدونتنا:


Source: https://habr.com/ru/post/ar437044/


All Articles