Thực trạng mong manh của xung đột phụ thuộc
Năm ngoái, tôi đã phải “trông nom” 14 microservice đang đứng trước bờ vực tan rã trong quá trình di chuyển. Chúng tôi có các ứng dụng Java 8 cũ chạy song song với các dịch vụ mới hơn được xây dựng trên Java 17 và 21. Tệ hơn nữa, các script xử lý dữ liệu là một mớ hỗn độn giữa Python 3.10 và 3.12. Team của tôi ban đầu đã cố gắng duy trì bằng cách hardcode đường dẫn tuyệt đối trong JAVA_HOME hoặc nhồi nhét hàng tá alias lộn xộn vào .bashrc. Đó quả thực là một ngôi nhà bằng bài mong manh.
Mọi thứ đổ vỡ khi lệnh apt upgrade định kỳ trên các node Ubuntu đã thay đổi /usr/bin/java toàn cục sang một phiên bản không tương thích với công cụ điều phối của chúng tôi. Sự cố ngừng hoạt động trong 15 phút đó là giọt nước tràn ly. Tôi quyết định chuẩn hóa hơn 40 máy chủ của chúng tôi bằng hệ thống update-alternatives. Tiện ích có sẵn của Linux này quản lý nhiều phiên bản của cùng một phần mềm mà không làm hỏng các phụ thuộc hệ thống vốn giúp hệ điều hành của bạn vận hành ổn định.
Bảng tổng đài: Cách hệ thống Alternatives vận hành
Hãy coi update-alternatives như một bảng tổng đài thông minh. Thay vì /usr/bin/java trỏ trực tiếp đến một tệp thực thi, nó tạo ra một lớp gián tiếp ba tầng:
- Tên chung (Generic Name):
/usr/bin/javaliên kết đến bảng tổng đài. - Liên kết đích (Destination Link):
/etc/alternatives/javađóng vai trò trung gian. - Tệp thực thi thực tế (Actual Binary): Đường dẫn cuối cùng sẽ phân giải đến
/usr/lib/jvm/java-17-openjdk-amd64/bin/java.
Bằng cách chỉ thay đổi liên kết trung gian, bạn có thể chuyển đổi phiên bản công cụ toàn cục. Hệ thống theo dõi các lựa chọn này trong một cơ sở dữ liệu đặt tại /var/lib/dpkg/alternatives/. Mỗi phiên bản có một điểm ưu tiên (priority score). Ở chế độ ‘auto’, hệ thống sẽ chọn phiên bản có số điểm cao nhất. Ở chế độ ‘manual’, lựa chọn của bạn sẽ được khóa lại và không thay đổi ngay cả khi bạn cài đặt một gói mới hơn.
Quản lý phiên bản Java chính xác
Java là nơi công cụ này phát huy sức mạnh nhất. Hầu hết các bản phân phối (distro) đều đặt các phiên bản OpenJDK vào /usr/lib/jvm/, nhưng không phải lúc nào chúng cũng được đăng ký chính xác. Đây là cách tôi thiết lập môi trường cho ba phiên bản cụ thể:
sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-8-openjdk-amd64/bin/java 1081
sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-17-openjdk-amd64/bin/java 1171
sudo update-alternatives --install /usr/bin/java java /usr/lib/jvm/java-21-openjdk-amd64/bin/java 1211
Các chữ số ở cuối (1081, 1171, 1211) xác định thứ bậc ưu tiên. Vì Java 21 có điểm cao nhất, nó sẽ trở thành mặc định. Để chuyển đổi phiên bản thủ công, hãy sử dụng flag config:
sudo update-alternatives --config java
Lệnh này sẽ mở ra một menu tương tác đơn giản. Trên các node Ubuntu 22.04 với 4GB RAM, thay đổi này thực sự đã cải thiện độ tin cậy khi khởi động. Chúng tôi đã loại bỏ các script wrapper cồng kềnh trước đây dùng để dò tìm JAVA_HOME. Giờ đây, nhân (kernel) xử lý việc phân giải symlink ngay lập tức.
Giữ cho Trình biên dịch và Công cụ luôn đồng bộ
Tôi thường thấy các kỹ sư cập nhật runtime java nhưng lại quên mất javac (trình biên dịch) hoặc jar. Điều này dẫn đến lỗi “Unsupported Class Version” trong quá trình build. Hãy luôn nhớ cấu hình đầy đủ bộ công cụ:
sudo update-alternatives --config javac
sudo update-alternatives --config jar
Python: Tôn trọng mặc định của hệ thống
Python lại là một câu chuyện hoàn toàn khác. Mặc dù venv rất tuyệt vời cho các dự án, đôi khi bạn cần kiểm soát những gì python3 thực hiện trong shell toàn cục. Tuy nhiên, bạn phải cực kỳ cẩn thận ở đây. Trên Ubuntu, các tiện ích hệ thống cốt lõi như apt và netplan phụ thuộc vào phiên bản Python cụ thể đi kèm với hệ điều hành.
Tôi đăng ký các phiên bản Python của mình như sau:
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 2
Một cảnh báo quan trọng: Nếu bạn chuyển python3 toàn cục sang một phiên bản mới hơn mà các script hệ thống đó không tương thích, bạn sẽ làm hỏng trình quản lý gói của mình. Tôi nghiêm ngặt giới hạn việc chuyển đổi toàn cục này cho các build agent và CI runner. Đối với các máy chủ web production, tôi để nguyên Python hệ thống và sử dụng môi trường ảo (virtual environment) thay thế.
Nhóm GCC và G++ với Slaves
Các backend yêu cầu hiệu suất cao thường cần các phiên bản GCC cụ thể để truy cập các flag tối ưu hóa mới. Bạn có thể sử dụng flag --slave để đảm bảo rằng gcc và g++ luôn được thay đổi cùng nhau như một cặp.
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 110 --slave /usr/bin/g++ g++ /usr/bin/g++-11
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 120 --slave /usr/bin/g++ g++ /usr/bin/g++-12
Liên kết này rất mạnh mẽ. Khi bạn chọn một phiên bản gcc mới thông qua menu cấu hình, trình biên dịch g++ sẽ tự động thay đổi theo. Điều này ngăn chặn cơn ác mộng khi biên dịch mã bằng một phiên bản nhưng lại liên kết (link) bằng một phiên bản khác.
Sáu tháng sau: Kết quả
Sau nửa năm vận hành trên 40 máy chủ, thành quả lớn nhất chính là tính nhất quán. Chúng tôi không còn nghe câu “nó chạy tốt trên máy của tôi” nữa. Mọi nhà phát triển đều biết rằng update-alternatives --display java là nguồn thông tin chuẩn duy nhất. Nếu bạn cần dọn dẹp một phiên bản cũ để giữ menu gọn gàng, chỉ cần một lệnh duy nhất:
sudo update-alternatives --remove java /usr/lib/jvm/java-8-openjdk-amd64/bin/java
Lời kết
Công cụ update-alternatives là một tiện ích Linux kinh điển thực hiện hoàn hảo một nhiệm vụ duy nhất. Nó lấp đầy khoảng trống giữa các thư mục hệ thống cứng nhắc và nhu cầu linh hoạt của quá trình phát triển hiện đại. Bằng cách loại bỏ việc tạo symlink thủ công, tôi đã ổn định hóa môi trường production và giảm gần 30% độ phức tạp của các script triển khai. Nếu bạn vẫn đang chỉnh sửa thủ công các liên kết trong /usr/bin, đã đến lúc để hệ thống xử lý gánh nặng đó thay bạn.

