Rust-ის გაშვებაზე კარგი ამბავი ისაა, რომ შედეგი ერთი ბინარული ფაილია, რომელსაც runtime არ სჭირდება, და ის ოც მეგაბაიტ მეხსიერებაში დატვირთულ საიტსაც კი ემსახურება. უხერხული ამბავი ისაა, რომ ამ ბინარულის მიღება არის ძვირი ნაწილი, და ძვირია სწორედ იმ რესურსში, რომელიც პატარა გეგმას ყველაზე ნაკლები აქვს. მოკრძალებული ვებ სერვისის release build-ს სჭირდება ერთი-ორი გიგაბაიტი RAM და რამდენიმე წუთი CPU; შედეგი კი უქმად იმის ნაწილში მუშაობს, რაც შესადარებელ Python ან Node სერვისს სჭირდება.
ამრიგად, Rust-ის გაშვების მთელი კითხვაა: სად ხდება build და რა სჭირდება გაშვებულ პროცესს შემდეგ. ეს პოსტი ორივეს პასუხობს, ასევე იმ პარამეტრებს, რომლებიც ცვლის build-ის დროსა და ბინარულის ზომას, როგორ მიება სწორად კონტეინერში, რამდენი tokio worker thread გჭირდება ბირთვის ნაწილზე და რა ტყდება.
რა სჭირდება Rust სერვისს გაშვებისას#
თითქმის არაფერი. კომპილატორი შენს კოდსა და დამოკიდებულებებს ერთ გასაშვებ ფაილში აკავშირებს. არ არის interpreter, არ არის ვირტუალური გარემო, არ არის node_modules და სერვერზე არ არის პაკეტების მენეჯერი. რაც ბინარულს მაინც სჭირდება:
- C ბიბლიოთეკა, რომელსაც ის დაუკავშირდა. ნაგულისხმევი target
x86_64-unknown-linux-gnuglibc-ს დინამიკურად უკავშირდება, ამიტომ ბინარულს სჭირდება glibc, სულ მცირე იმდენად ახალი, რამდენადაც ის, რომლითაც აშენდა. ეს არის მიზეზი შეცდომისაversion 'GLIBC_2.34' not found, როცა ახალ დისტრიბუციაზე აგებული ბინარული ძველზე გადააქვთ.x86_64-unknown-linux-musl-ზე build სტატიკურად დაკავშირებულ ბინარულს იძლევა, რომელიც ყველგან მუშაობს, მცირე შესრულების ფასად ბევრი გამოყოფის მქონე სამუშაოში. - TLS სერტიფიკატები, თუ ის გამავალ HTTPS გამოძახებებს აკეთებს.
native-tls-ის crate-ები სისტემის საცავს კითხულობს;rustlswebpki-roots-ით საკუთარს ატარებს, რაც ერთით ნაკლები დამოკიდებულებაა. - მეხსიერება, რომელიც ათობით მეგაბაიტით იზომება. პატარა axum სერვისი მონაცემთა ბაზის pool-ით ჩვეულებრივ 20 MB-ზე ნაკლები resident მეხსიერებით უქმად დგას და იზრდება კავშირებისა და buffer-ების მიხედვით და არა კოდის ზომით.
ერთი glibc დეტალი ღირს ცოდნად, სანამ მას დაბნეული შეხვდები. კონკურენტულობისას glibc-ის allocator თითო thread-ზე arena-ებს ქმნის და გათავისუფლებულ მეხსიერებას იშვიათად აბრუნებს ოპერაციულ სისტემაში, ამიტომ resident მეხსიერება შეიძლება გაიზარდეს და შემდეგ იმ დონეზე გაჩერდეს, რომელიც ბევრად აღემატება იმას, რასაც შენი პროგრამა ნამდვილად ინახავს. გარემოში MALLOC_ARENA_MAX=2-ის დაყენება ჩვეულებრივ მას ასწორებს, ხოლო jemalloc-ზე ან mimalloc-ზე გადასვლა crate-ის საშუალებით უფრო მძიმე გამოსავალია. ამას ნუ დაედევნები, სანამ გრაფიკი, რომელიც მას აჩვენებს, არ გაქვს.
axum თუ actix-web#
ორივე მომწიფებულია, ორივე საკმარისად სწრაფია, რომ შენი მონაცემთა ბაზა იყოს შემზღუდველი, და ორივე tokio-ზე მუშაობს. განსხვავება კოდის ფორმაშია.
axum აგებულია hyper-სა და tower-ზე, ამიტომ მისი middleware არის tower middleware, რომელსაც ფართო ეკოსისტემაც იყენებს. handler-ები უბრალო async ფუნქციებია, ხოლო routing - მონაცემები:
use axum::{routing::get, Router};use tokio::net::TcpListener;#[tokio::main]async fn main() { let app = Router::new().route("/health", get(|| async { "ok" })); let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8000".into()); let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap(); axum::serve(listener, app).await.unwrap();}actix-web-ს აქვს actor-იდან მომდინარე საკუთარი runtime tokio-ს თავზე, საკუთარი middleware trait-ები და builder სტილის სერვერი:
HttpServer::new(|| App::new().route("/health", web::get().to(health))) .workers(2) .bind(("0.0.0.0", port))? .run() .awaitაირჩიე axum, თუ tower ფენების ხელახლა გამოყენება და hyper ეკოსისტემა გინდა, actix-web - თუ მისი API გირჩევნია ან რაღაც გჭირდება, რაც მხოლოდ მას აქვს. არცერთი benchmark-ის რიცხვებით არ აირჩიო; მათ შორის სხვაობა ბევრად უფრო მცირეა, ვიდრე სხვაობა მონაცემთა ბაზასთან ერთ round trip-სა და ორს შორის.
გაითვალისწინე .workers(2) actix-ის მაგალითში. ნაგულისხმევად ის თითო ლოგიკურ CPU-ზე ერთ worker-ს უშვებს, რაც იგივე ხაფანგია, რომელიც ქვემოთ tokio-სთვის განიხილება.
Build: debug, release და პროფილი, რომელიც მნიშვნელოვანია#
cargo build გამოიმუშავებს debug ბინარულს: ოპტიმიზაციის გარეშე, overflow შემოწმებებით და ხშირად გაშვებისას ხუთიდან ათჯერ უფრო ნელს. ის არასოდეს არის ის, რასაც აშვებ. cargo build --release გამოიმუშავებს ოპტიმიზებულ ბინარულს target/release/<name>-ში.
$ cargo build --release --locked$ ./target/release/api--locked build-ს აძლევს ვარდნას იმის ნაცვლად, რომ Cargo.lock ჩუმად განაახლოს. აპლიკაციისთვის - განსხვავებით ბიბლიოთეკისგან - lock ფაილი repository-ში უნდა იყოს, ხოლო --locked არის ის, რაც მის commit-ს აზრს სძენს.
release პროფილი რეგულირებადია, და ღილაკები build-ის დროს ცვლის გაშვების სიჩქარესა და ბინარულის ზომაზე:
[profile.release]lto = "thin"codegen-units = 1strip = "symbols"| პარამეტრი | ნაგულისხმევი release-ში | რას ცვლის მისი შეცვლა |
|---|---|---|
opt-level | 3 | "s" ან "z" სიჩქარის ნაცვლად ზომაზე ოპტიმიზაციას აკეთებს |
lto | false | "thin" ან true crate-ებს შორის inline-ს აკეთებს; ნელი, build-ის მეხსიერება მეტია |
codegen-units | 16 | 1 უკეთეს კოდს და უფრო ნელ, მეხსიერების მჭამელ build-ს იძლევა |
strip | false | "symbols" debug სიმბოლოებს შლის, ხშირად ბინარულს ნახევრად ამცირებს |
debug | false | 1 ინახავს ხაზების ცხრილებს, რომ panic-ებს სასარგებლო backtrace ჰქონდეს |
panic | "unwind" | "abort" უფრო მცირე და სწრაფია, მაგრამ ნებისმიერი panic პროცესს კლავს |
panic = "abort" პაუზას იმსახურებს. unwinding-ით axum handler-ში panic იმ მოთხოვნის task-ს კლავს და პროცესი აგრძელებს მუშაობას, ხოლო tower_http-ის catch-panic ფენას შეუძლია ის 500-ად აქციოს. abort-ით ერთი მოთხოვნის ერთი panic მთელ სერვისს ჩამოაგდებს და კონტეინერი გადაიტვირთება. ეს ავტომატურად არასწორი არ არის - ჩავარდნა, რომელიც სუფთად იწყება, ზოგჯერ უკეთესია, ვიდრე პროცესი უცნობ მდგომარეობაში - მაგრამ აირჩიე ეს შეგნებულად.
ასევე ღირს ცოდნად: ინკრემენტული კომპილაცია release პროფილში გამორთულია, ამიტომ release build ყოველ ჯერზე შენს crate-ს ნულიდან აკომპილირებს. დამოკიდებულებები მაინც ქეშირდება target/-ში, სწორედ ამიტომ არის პირველი build გრძელი და მეათე მოკლე. არასოდეს გააკეთო cargo clean deploy-ის ნაწილად.
Build-ის დრო და მეხსიერება პატარა გეგმაზე#
ეს არის ნაწილი, რომელიც შენს გეგმას წყვეტს. კომპილატორი მეხსიერების მჭამელი პროგრამაა, ხოლო lto-თი და codegen-units = 1-ით ბოლო linking პიკია.
| პროექტის ზომა | პირველი release build | შენი crate-ის ხელახალი build | Build-ის პიკური მეხსიერება |
|---|---|---|---|
| პატარა სერვისი, ~30 crate | 1-3 წუთი | 10-30 წამი | ~1 GB |
| ტიპური ვებ სერვისი, 150-300 crate | 4-12 წუთი | 20-60 წამი | 1-2 GB |
| მძიმე ხე ჩართული LTO-თი | 15 წუთი ან მეტი | 1-3 წუთი | 2-4 GB |
ეს გამოცდილებიდან მიღებული დიაპაზონებია და არა დაპირებები - რეალური მაჩვენებელი დამოკიდებულია შენს დამოკიდებულებების ხეზე, მის რა ნაწილშია ბევრი მაკრო და შენ მიერ ნაყიდ CPU წილზე. აქედან სამი რამ გამომდინარეობს:
- ყველაზე პატარა საფეხური გაშვებისთვისაა და არა build-ისთვის. 1 GB-ით და ნახევარი ბირთვით ნებისმიერი არატრივიალური რამის build ან ოც წუთს გრძელდება, ან ჩერდება, როცა მეხსიერების ლიმიტს აღწევს. RE:NODE-ზე კონტეინერი ლიმიტზე ჩერდება და სუფთად იწყება ხელახლა და არა swap-ში დარჩენის უფლებას იღებს, ამიტომ ზედმეტად ამბიციური build ჩანს, როგორც restart კომპილაციის შუაში და არა როგორც ნელი.
- პარალელიზმი შეზღუდე ამბიციამდე.
cargo build -j 1ანCARGO_BUILD_JOBS=1ერთ crate-ს აკომპილირებს ერთდროულად და პიკურ მეხსიერებას მნიშვნელოვნად ამცირებს, კედლის საათის დროის ხარჯზე. ეს განსხვავებაა build-ს შორის, რომელიც სრულდება, და იმას შორის, რომელიც არა. - LTO ბოლოს ჩართე. ააგე ნაგულისხმევით, სანამ სერვისი არ ამუშავდება.
lto = "thin"დაcodegen-units = 1ღირს გაშვების სიჩქარის რამდენიმე პროცენტად და უფრო მცირე ბინარულად; ისინი არ ღირს სერვერად, რომელსაც build არ შეუძლია.
ალტერნატივაა build სხვაგან და ბინარულის გაგზავნა. ააგე ლოკალურად ან CI-ში სწორი target-ისთვის, შემდეგ ატვირთე გასაშვები ფაილი SFTP-ით და chmod +x გაუკეთე. ეს ლეგიტიმური გზაა Rust-ის 1 GB გეგმაზე გასაშვებად, და მისი ფასი ისაა, რომ გაშვებულ არტეფაქტს სერვერი აღარ აწარმოებს წყაროდან, რომლის ნახვაც შეგიძლია - build რეპროდუცირებადი შეინარჩუნე და თუ შეგიძლია, commit hash ბინარულში ჩასვი.
ამ ქეშის დისკის ღირებულება რეალურია: target/ დირექტორია საშუალო ვებ სერვისისთვის ჩვეულებრივ 1-3 GB-ია, როცა debug და release არტეფაქტები ორივე მასშია. აპლიკაციის გეგმები 5 GB დისკით იწყება, ამიტომ სერვერზე build-ისას მას უნდა უყურო. target/debug-ის წაშლა ჩვეულებრივ ყველაზე ადვილი გიგაბაიტია, რასაც ოდესმე გაათავისუფლებ.
Binding, პორტები და thread-ები#
ორი პარამეტრი, რომელთაგან ორივეს ნაგულისხმევი კონტეინერში არასწორია.
პირველია bind მისამართი. 127.0.0.1 კონტეინერის გარეთ ვერაფერს აღწევს; გამოიყენე 0.0.0.0 და პორტი აიღე გარემოდან და არა მყარად ჩაწერე. Pterodactyl-ზე დაფუძნებულ პანელებზე გამოყოფილი პორტი მოდის გარემოს ცვლადად, რომელიც ჩანს Startup ჩანართზე; სხვაგან ის ხშირად PORT-ია.
მეორეა tokio worker thread-ების რაოდენობა. Tokio თავისი multi-threaded runtime-ის ზომას available_parallelism()-იდან იღებს. Rust-ის ახალი ვერსიები Linux-ზე cgroup CPU კვოტებს მართლაც ითვალისწინებენ, მაგრამ შედეგი მრგვალდება და არასოდეს არის ერთზე ნაკლები, ამიტომ ბირთვის ნაწილამდე შეზღუდულ კონტეინერს მაინც შეიძლება ჰქონდეს უფრო მეტი scheduler thread, ვიდრე მას გამოყენება შეუძლია. მისი აშკარად დაყენება ერთ ხაზს ჯდება და კითხვას აქრობს:
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]async fn main() { }ერთი ან ორი worker thread სწორია 1-2 GB საფეხურებისთვის; ამაზე მაღლა CPU წილს მიესადაგე. მეტი thread მკაცრ CPU კვოტაზე გამტარუნარიანობას არ ამატებს, ის ამატებს კონტექსტის გადართვას და, კვოტაზე, უფრო გრძელ შეჩერებებს, როცა კონტეინერი იზღუდება.
კონფიგურაცია თავად ჩვეულებრივი გარემოს ცვლადებია. std::env::var რამდენიმესთვის საკმარისია; envy ან figment მთელ გარემოს struct-ში აბრუნებენ და გაშვებისას ვარდებიან, თუ რამე აკლია, რაც სწორედ ის ქცევაა, რაც გინდა. რომელსაც არ უნდა იყენებდე, ყოველი ცვლადი წაიკითხე ერთხელ გაშვებისას, რომ დაკარგული მნიშვნელობა პროცესს მაშინვე გააჩერებდეს და არა იშვიათ კოდის გზაზე ღამის სამ საათზე. გარემოს ცვლადები და საიდუმლოებები ამბობს, სად უნდა იყოს ისინი.
Proxy-ს უკან: header-ები, websocket-ები და TLS#
სერვისში TLS-ის დასრულება შესაძლებელია axum-server-ითა და rustls-ით, და ჰოსტინგის გეგმაზე ჩვეულებრივ არასწორი არჩევანია: მაშინ შენ გეკუთვნის სერტიფიკატის განახლება, HTTP-დან HTTPS-ზე გადამისამართება და გადატვირთვა სერტიფიკატის შეცვლისას. დაუთმე ეს proxy-ს და შენს გამოყოფილ პორტზე მომსახურე უბრალო HTTP.
რას ნიშნავს ეს შენი კოდისთვის:
- კლიენტის მისამართი არის
X-Forwarded-For-ში და არა კავშირში. axum-შიConnectInfoproxy-ს გაძლევს; tower ფენა ან header-ის ხელით წაკითხვა კლიენტს გაძლევს. ენდე header-ს მხოლოდ იმიტომ, რომ იცი, რომ proxy-მ დააყენა. - სქემა არის
X-Forwarded-Proto-ში. ყველგან, სადაც აბსოლუტურ URL-ს აგებ - გადამისამართებები, ბმულები ელფოსტაში, OAuth callback-ები - წაიკითხე ის, თორემ ადამიანებსhttp://-ზე გააგზავნი და გადამისამართების ციკლს გამოიწვევ. - Websocket-ებს სჭირდება upgrade header-ების გადაცემა და idle timeout, რომელიც შენს heartbeat-ზე გრძელია. Websocket-ები reverse proxy-ს უკან მთელი ამბავია და ის იდენტურად ეხება
axum::extract::ws-ს.
აპლიკაციის გეგმებში proxy slot შედის: მიუთითე A ჩანაწერი ნაჩვენებ მისამართზე და სერტიფიკატი გაიცემა და ავტომატურად განახლდება 21-დღიან ფანჯარაში, ხოლო კლიენტის რეალური მისამართი X-Forwarded-For-ში მოდის. რას აკეთებს reverse proxy header-ების დანარჩენ ჯაჭვს ხსნის.
ლოგირება, panic-ები და სუფთა გამორთვა#
გამოიყენე tracing tracing-subscriber-ით, ფილტრი წაიკითხე გარემოდან და ჩაწერე standard output-ზე, რომ კონსოლს ჰქონდეს:
RUST_LOG=info,tower_http=debug,sqlx=warnRUST_BACKTRACE=1RUST_BACKTRACE=1 არაფერი ღირს, სანამ panic არ მოხდება, და მერე ეს განსხვავებაა სასარგებლო ანგარიშსა და სიტყვა "panicked"-ს შორის. შეინარჩუნე debug = 1 release პროფილში, თუ ამ backtrace-ებში ხაზების ნომრები გინდა; ის ბინარულს ზრდის, მაგრამ არა გამოყენებულ მეხსიერებას.
დაამუშავე SIGTERM, რადგან სწორედ ამას აგზავნის stop ან restart. Axum-ში გამორთვა ჩაშენებულია:
axum::serve(listener, app) .with_graceful_shutdown(shutdown_signal()) .await .unwrap();shutdown_signal-ის შიგნით დაელოდე ორივეს - tokio::signal::ctrl_c()-ს და SignalKind::terminate() stream-ს - და შემდეგ დააბრუნე. Serve ახალ კავშირებს აღარ იღებს, მიმდინარე მოთხოვნებს ასრულებს და გადის. მის გარეშე პროცესი დამატებითი ვადის შემდეგ კვდება და რაც მიმდინარეობდა, იკარგება - ნახევრად ჩაწერილი პასუხისა და buffer-ში მყოფი სამუშაოს ჩათვლით. Graceful shutdown და health check-ები health endpoint-ის მხარეს მოიცავს, რომელიც Rust-ისთვის არის route, რომელიც მუდმივას აბრუნებს და მეტს არაფერს.
მონაცემთა ბაზები და connection pool-ები#
sqlx და tokio-postgres deadpool-ით ჩვეულებრივი არჩევანია. რიცხვი, რომელიც მნიშვნელოვანია, არის pool-ის ზომა, რადგან pool თავის კავშირებს ღიად ინახავს, იყენებ მათ თუ არა:
PgPoolOptions::new() .max_connections(5) .acquire_timeout(Duration::from_secs(5)) .connect(&database_url) .await?ხუთი გონივრული ნაგულისხმევია სერვისისთვის ერთ ან ორ ბირთვზე. Rust სერვისს ერთი pool აქვს პროცესზე და არა worker-ზე, რაც რეალური უპირატესობაა gunicorn-ის process-per-worker მოდელთან შედარებით - ათი Python worker თითო pool-ით ორმოცდაათი კავშირია, სადაც ექვივალენტური Rust სერვისი ხუთია. Connection pool-ები და ლიმიტები ამ კავშირის მეორე მხარის არითმეტიკას შეიცავს.
ერთი deploy-ის სპეციფიკური ხაფანგი: sqlx-ის compile-time შემოწმებულ query-ებს build-ისას ცოცხალი მონაცემთა ბაზა სჭირდება, თუ მათ offline არ მოამზადებ. გაუშვი cargo sqlx prepare ლოკალურად, commit-ში ჩასვი მის მიერ გენერირებული .sqlx დირექტორია და build-ის გარემოში დააყენე SQLX_OFFLINE=true. სხვაგვარად შენი სერვერის build ვარდება "set DATABASE_URL to use query macros"-ით ყველაზე შეუფერებელ მომენტში. პანელის database slot-ებს მოჰყვება გენერირებული host, მომხმარებელი და პაროლი; ცალკე PostgreSQL ან MongoDB ინსტანცია არის მონაცემთა ბაზის ჰოსტინგის გეგმა.
რა ტყდება#
`linker 'cc' not found`. Build-ს C toolchain სჭირდება crate-ებისთვის, რომლებსაც native კოდი აქვთ. მართულ გეგმაზე ეს image-ის ნაწილია; შენს მანქანაზე დააყენე დისტრიბუციის build essentials პაკეტი.
Build ჩერდება შეცდომის გარეშე. მეხსიერება ამოიწურა. გადადი -j 1-ზე, გამორთე LTO ან ააგე იქ, სადაც მეტი RAM არის, და ატვირთე ბინარული.
`version 'GLIBC_2.34' not found`. ბინარული აგებულია glibc-ზე, რომელიც სერვერზე არსებულზე ახალია. ააგე უფრო ძველ ბაზაზე ან აირჩიე musl სტატიკური ბინარულისთვის.
`Text file busy` ბინარულის ჩანაცვლებისას. ზემოდან წერ გასაშვებ ფაილს, რომელიც მუშაობს. ჯერ გააჩერე სერვისი ან ჩაწერე ახალ სახელზე და ადგილზე გადაიტანე.
Connection refused, მაგრამ ლოგი ამბობს, რომ უსმენს. მიბმულია 127.0.0.1-ზე. მიაბი 0.0.0.0-ზე.
პირველი deploy ათ წუთს გრძელდება, შემდეგ ერთი ისევ ათ წუთს. დამოკიდებულების ვერსია შეიცვალა, ამიტომ ქეშირებული არტეფაქტები გაუქმდა. ეს მოსალოდნელია Cargo.lock-ის განახლების შემდეგ; თუ ეს ყოველ deploy-ზე ხდება, რაღაც target/-ს ასუფთავებს.
მეხსიერება იზრდება დატვირთვისას და არასოდეს ბრუნდება ქვემოთ. ალბათ allocator-ის arena-ებია და არა გაჟონვა. სცადე MALLOC_ARENA_MAX=2 და უყურე გრაფიკს, სანამ რამეს გადაწერ.
Panic კლიენტს 500-ის ნაცვლად არაფერს აბრუნებს. catch-panic ფენის გარეშე task კვდება და კავშირი წყდება. დაამატე tower_http::catch_panic::CatchPanicLayer, რომ კლიენტმა პასუხი მიიღოს და შენმა ლოგებმა panic.
FAQ#
რამდენი მეხსიერება სჭირდება Rust ვებ სერვისს?
გაშვებისას ბევრად ნაკლები, ვიდრე ელოდები: პატარა axum სერვისი მონაცემთა ბაზის pool-ით ჩვეულებრივ 20 MB-ზე ნაკლებით უქმად დგას და რეალურ ტრაფიკს 100 MB-ში ემსახურება. Rust-ის მეხსიერების კითხვა build-ზეა და არა პროცესზე - სწორედ ამას სჭირდება გიგაბაიტი ან მეტი.
შემიძლია build ყველაზე პატარა გეგმაზე?
პატარა პროექტისთვის - დიახ, -j 1-ით და LTO-ს გარეშე. სერვისი, რომლის ხეში რამდენიმე ასეული crate არის, 1 GB-ზე და ნახევარ ბირთვზე მტკივნეული იქნება. ან გადადი ერთი საფეხურით მაღლა გეგმაზე, რომელზეც აგებ, ან ააგე CI-ში და ატვირთე ბინარული.
axum თუ actix-web?
ორივე. axum უკეთ ერგება, თუ tower middleware და hyper ეკოსისტემა გინდა; actix-web-ს საკუთარი კარგად დოკუმენტირებული სამყარო აქვს და ისეთივე production-ready არის. შესრულება გადამწყვეტი ფაქტორი არ არის იმ მასშტაბზე, სადაც არჩევანი კეთდება.
მჭირდება reverse proxy მის წინ?
გჭირდება რაღაც, რაც TLS-ს ასრულებს და სერტიფიკატს ანახლებს, და proxy ამის ყველაზე მარტივი საშუალებაა. თავად სერვისი კომპეტენტური HTTP სერვერია, ამიტომ proxy არის სერტიფიკატებისთვის, გადაცემული header-ებისთვის და მოთხოვნის ლიმიტებისთვის და არა სისწრაფისთვის.
Cargo.lock უნდა იყოს commit-ში?
ბინარულისთვის - დიახ, ყოველთვის, და აგე --locked-ით, რომ deploy ხმამაღლა დავარდეს და არა რაღაც ახალი გადაწყვიტოს. ბიბლიოთეკისთვის კონვენცია საპირისპიროა, რაც დაბნეულობის წყაროა.
რატომ არაფერს აჩვენებს პანელის კონსოლი?
Rust standard output-ზე ბუფერის გარეშე წერს, როცა ის ტერმინალი არ არის, ამიტომ ჩვეულებრივი მიზეზი ისაა, რომ ჯერ არაფერი დაულოგავს - სერვისი, რომელსაც tracing subscriber არ აქვს, საერთოდ არაფერს ბეჭდავს, რამდენსაც არ უნდა აკეთებდეს. დააყენე subscriber და დააყენე RUST_LOG.




კომენტარები
სრულიად ანონიმურად: ანგარიშის, ელფოსტის და cookie-ის გარეშე. ინახება მხოლოდ სახელი, ტექსტი და დრო - სხვა არაფერი. ბმულების რაოდენობა ლიმიტირებულია.