Lập luận của đối thủ khiến tôi nhớ đến một kẻ ngoại đạo, người, khi được hỏi về thế giới đang đứng trên cái gfi, đã trả lời: "Trên một con rùa." Nhưng con rùa đứng trên cái gì? "Trên một con rùa khác."
—Joseph Barker (1854)
Vì vậy, bạn biết microservices là gì và hy vọng hiểu được những lợi ích chính của chúng. Bây giờ có lẽ bạn đang háo hức muốn đi và bắt đầu tạo ra chúng, phải không? Nhưng bắt đầu từ đâu? Trong chương này, chúng ta sẽ xem xét cách suy nghĩ về ranh giới của các microservice của bạn, hy vọng sẽ tối đa hóa những ưu điểm và tránh một số nhược điểm tiềm ẩn. Nhưng trước tiên, chúng ta cần một cái gì đó để làm việc cùng nó.
Giới thiệu về MusicCorp
Sách về ý tưởng hoạt động tốt hơn với các ví dụ. Nếu có thể, tôi sẽ chia sẻ những câu chuyện từ các tình huống thực tế, nhưng tôi nhận thấy rằng việc có một lĩnh vực hư cấu để hoạt động cũng rất hữu ích. Xuyên suốt cuốn sách, chúng ta sẽ quay trở lại lĩnh vực này, xem khái niệm microservices hoạt động như thế nào trong thế giới này.
Vì vậy, chúng ta hãy chuyển sự chú ý của chúng ta đến một trong những nhà bán lẻ trực tuyến tiên tiến nhất hiện nay – MusicCorp. MusicCorp gần đây là một nhà bán lẻ truyền thống, nhưng sau khi công ty từ bỏ mảng kinh doanh máy hát, mảng kinh doanh nền tảng của MusicCorp, họ ngày càng tập trung nhiều hơn vào việc kinh doanh trực tuyến. Công ty có một trang web, nhưng cảm thấy rằng bây giờ là lúc để tăng gấp đôi doanh số trên các nền tảng trực tuyến. Xét cho cùng, những chiếc iPod đó chỉ là một mốt nhất thời (rõ ràng là Zunes tốt hơn nhiều) và những người hâm mộ âm nhạc đang khá vui khi chờ đĩa CD được chuyển tới nhà của họ. Chất lượng bao giờ cũng hơn sự tiện lợi, phải không? Và trong khi chúng ta đang ở đó, điều mà mọi người vẫn tiếp tục về Spotify này là gì — một nền tảng hướng đến đối tượng thanh thiếu niên?
Mặc dù đi sau một chút so với phần còn lai, nhưng MusicCorp lại có tham vọng lớn. May mắn thay, họ đã quyết định rằng cơ hội tốt nhất để chiếm lấy thị phần là đảm bảo rằng nó có thể thực hiện những thay đổi dễ dàng nhất có thể. Microservices giành chiến thắng!
Điều gì tạo nên một dịch vụ tốt?
Trước khi nhóm từ MusicCorp thay đổi chính mình, tạo ra dịch vụ này đến dịch vụ khác trong nỗ lực cung cấp loại băng 8 bản nhạc cho tất cả mọi người và một vài thứ khác, hãy bắt đầu và nói một chút về ý tưởng cơ bản quan trọng nhất mà chúng ta cần ghi nhớ. Điều gì tạo nên một dịch vụ tốt? Nếu bạn vẫn sống sót sau một lần deploy SOA không thành công, bạn có thể có một số ý tưởng về nơi tiếp theo mà tôi nói đến. Nhưng đề phòng trong trường hợp bạn không phải là kẻ may mắn hoặc giả sử bạn chính là kẻ may mắn, tôi muốn bạn tập trung vào hai khái niệm chính: liên kết lỏng lẻo và tính liên kết cao (loose coupling and high cohesion). Chúng tôi sẽ nói chi tiết trong suốt cuốn sách về những ý tưởng và thực tiễn khác, nhưng chúng đều vô ích nếu chúng tôi làm sai hai điều này.
Mặc dù thực tế là hai thuật ngữ này được sử dụng rất nhiều, đặc biệt là trong ngữ cảnh của các hệ thống hướng đối tượng, nhưng điều đáng bàn về ý nghĩa của chúng đối với microservices là gì.
Liên kết lỏng lẻo
Khi các dịch vụ được kết hợp một cách lỏng lẻo, một sự thay đổi đối với một dịch vụ không yêu cầu thay đổi một dịch vụ khác. Toàn bộ quan điểm của microservice là có thể thực hiện thay đổi đối với một dịch vụ và deploy nó mà không cần thay đổi bất kỳ phần nào khác của hệ thống. Điều này thực sự khá quan trọng.
Những thứ gì gây ra sự kết hợp chặt chẽ? Một sai lầm cổ điển là chọn một phong cách tích hợp liên kết chặt chẽ giữa dịch vụ này với dịch vụ khác, khiến những thay đổi bên trong dịch vụ đòi hỏi nhưng nơi sử dụng nó phải thay đổi. Chúng ta sẽ thảo luận sâu hơn về cách tránh điều này trong Chương 4.
Một dịch vụ được kết hợp một cách lỏng lẻo cần biết rất ít về những dịch vụ mà nó cộng tác cùng. Điều này cũng có nghĩa là chúng tôi có thể muốn giới hạn số lượng các loại yêu cầu/gọi chức năng khác nhau từ dịch vụ này sang dịch vụ khác, bởi vì ngoài vấn đề tiềm tàng về hiệu suất, giao tiếp kiểu này có thể dẫn đến kết nối chặt chẽ.
Độ gắn kết cao
Chúng tôi muốn hành vi liên quan thi đi cùng nhau và hành vi không liên quan nên ở nơi khác. Tại sao? Vâng, nếu chúng ta muốn thay đổi hành vi, chúng ta muốn có thể thay đổi nó ở một nơi và giải phóng thay đổi đó càng sớm càng tốt. Nếu chúng tôi phải thay đổi hành vi đó ở nhiều nơi khác nhau, chúng tôi sẽ phải release nhiều dịch vụ khác nhau (có thể cùng một lúc) để thực hiện thay đổi đó. Việc thực hiện các thay đổi ở nhiều nơi khác nhau sẽ chậm hơn và việc deploy nhiều dịch vụ cùng một lúc là rất rủi ro — cả hai điều này chúng tôi đều muốn tránh.
Vì vậy, chúng tôi muốn tìm các ranh giới của các vấn đề trong lĩnh vực của mình để giúp đảm bảo rằng hành vi liên quan sẽ ở cùng một nơi và giao tiếp với các ranh giới khác một cách lỏng lẻo nhất có thể.
Bối cảnh có giới hạn
Cuốn sách của Eric Evans về Thiết kế theo hướng lĩnh vực – Domain-Driven Design (Addison-Wesley) tập trung vào cách tạo hệ thống mô hình hóa các lĩnh vực trong thế giới thực. Cuốn sách chứa đầy những ý tưởng tuyệt vời như sử dụng ngôn ngữ phổ biến, sự trừu tượng của kho lưu trữ, và những thứ tương tự, nhưng có một khái niệm rất quan trọng mà Evans giới thiệu lúc đầu đã hoàn toàn lướt qua tôi: bối cảnh có giới hạn. Ý tưởng ở đây là bất kỳ lĩnh vực cụ thể nào đều bao gồm nhiều bối cảnh có giới hạn và nằm trong mỗi bối cảnh là những thứ (Eric sử dụng mô hình – model từ rất nhiều, có lẽ tốt hơn là những thứ – things) không cần giao tiếp với bên ngoài cũng như những thứ được chia sẻ ra bên ngoài với các bối cảnh có giới hạn khác. Mỗi bối cảnh có giới hạn có một giao diện rõ ràng, nơi nó quyết định những mô hình nào sẽ chia sẻ với các bối cảnh khác.
Một định nghĩa khác về bối cảnh có giới hạn mà tôi thích là "một trách nhiệm cụ thể được thực thi bởi các ranh giới rõ ràng." Nếu bạn muốn thông tin từ bối cảnh có giới hạn hoặc muốn đưa ra các yêu cầu về chức năng trong bối cảnh có giới hạn, bạn giao tiếp với các ranh giới rõ ràng của nó bằng cách sử dụng các mô hình. Trong cuốn sách của mình, Evans sử dụng một định nghĩa tương tự của các tế bào, nơi mà "[c]ells (tế bào) có thể tồn tại bởi vì màng của chúng xác định những gì ra/vào và xác định những gì có thể đi qua."
Hãy quay lại một chút với công việc kinh doanh của MusicCorp. Lĩnh vực của chúng tôi là toàn bộ hoạt động kinh doanh mà chúng tôi có. Nó bao gồm tất cả mọi thứ từ nhà kho đến bàn tiếp tân, từ tài chính đến đặt hàng. Chúng tôi có thể hoặc không thể mô hình hóa tất cả những điều đó trong phần mềm của mình, nhưng đó vẫn là thứ mà chúng tôi đang điều hành. Chúng ta hãy nghĩ về các phần của lĩnh vực đó trông giống như bối cảnh có giới hạn mà Evans đề cập đến. Tại MusicCorp, kho hàng của chúng tôi là một tổ hợp nhiều hoạt động — quản lý các đơn hàng được chuyển đi (và trả lại), nhận hàng mới, tổ chức các cuộc đua xe nâng, v.v. Ở những nơi khác, bộ phận tài chính có lẽ ít thú vị hơn, nhưng vẫn có một chức năng rất quan trọng trong tổ chức của chúng tôi. Những nhân viên này quản lý bảng lương, giữ các tài khoản của công ty và đưa ra các báo cáo quan trọng. Rất nhiều báo cáo. Chúng có lẽ cũng có những món đồ chơi trên bàn thú vị.
Các mô hình được chia sẻ và mô hình ẩn
Đối với MusicCorp, chúng ta có thể coi bộ phận tài chính và kho hàng là hai bối cảnh có giới hạn riêng biệt. Cả hai đều có giao diện rõ ràng với thế giới bên ngoài (về báo cáo hàng tồn kho, phiếu thanh toán, v.v.) và chúng có thông tin chi tiết mà chỉ họ cần biết (xe nâng, máy tính).
Giờ đây, bộ phận tài chính không cần biết về hoạt động chi tiết bên trong của nhà kho. Tuy nhiên, nó cần phải biết một số điều — ví dụ như nó cần biết về lượng hàng tồn kho để giữ cho các tài khoản được cập nhật. Hình 3-1 cho thấy một ví dụ về sơ đồ bối cảnh (context diagram). Chúng tôi thấy các khái niệm nội bộ của nhà kho, như Người chọn (người chọn đơn hàng), giá đang chứa hàng hóa, v.v. Tương tự như vậy, sổ cái tổng của công ty là không thể thiếu đối với tài chính nhưng không được chia sẻ ra bên ngoài ở đây.
Hình 3-1. Mô hình chia sẻ giữa bộ phận tài chính và kho hàng
Tuy nhiên, để có thể xác định giá trị của công ty, các nhân viên tài chính cần thông tin về cổ phiếu mà chúng tôi nắm giữ. Mặt hàng tồn kho sau đó trở thành một mô hình được chia sẻ giữa hai bối cảnh. Tuy nhiên, lưu ý rằng chúng ta không cần phải tiết lộ mọi thứ về mặt hàng trong kho từ bối cảnh kho hàng một cách mù quáng. Ví dụ: mặc dù nội bộ chúng tôi lưu giữ hồ sơ về một mặt hàng trong kho về nơi nó sẽ được đặt trong nhà kho, nhưng mặt hàng đó không cần phải được tiết lộ thông tin đó trong mô hình dùng chung. Vì vậy, sẽ có sự phân biệt giữa đại diện chỉ bên trong và đại diện bên ngoài mà chúng tôi đưa ra. Theo nhiều cách, điều này báo trước về cuộc thảo luận xung quanh REST trong Chương 4.
Đôi khi chúng ta có thể bắt gặp các mô hình có cùng tên nhưng có ý nghĩa rất khác nhau trong các bối cảnh khác nhau. Ví dụ: chúng ta có thể có khái niệm trả lại, đại diện cho việc khách hàng gửi lại thứ gì đó. Trong bối cảnh của khách hàng, trả lại là tất cả những gì liên quan đến việc in nhãn vận chuyển, gửi một gói hàng và chờ hoàn lại tiền. Đối với nhà kho, điều này có thể đại diện cho một gói hàng sắp được chuyển đến và một mặt hàng trong kho cần được bổ sung. Sau đó, trong kho chúng tôi lưu trữ thông tin bổ sung liên quan đến việc trả lại liên quan đến các nhiệm vụ sẽ được thực hiện; ví dụ: chúng tôi có thể tạo một yêu cầu khôi phục lại. Kết quả của mô hình chia sẻ là các quy trình khác nhau sẽ trở nên liên kết và hỗ trợ các thực thể trong mỗi bối cảnh có giới hạn, nhưng đó là mối quan tâm nội bộ trong chính bối cảnh đó.
Mô-đun và Dịch vụ
Bằng cách suy nghĩ rõ ràng về những mô hình nào nên được chia sẻ và không chia sẻ các đại diện nội bộ của chúng tôi, chúng tôi tránh được một trong những cạm bẫy tiềm ẩn có thể dẫn đến kết hợp chặt chẽ (ngược lại với những gì chúng tôi muốn). Chúng tôi cũng đã xác định một ranh giới trong lĩnh vực của chúng tôi, nơi tất cả các khả năng kinh doanh có cùng chí hướng sẽ tồn tại, mang lại cho chúng tôi sự gắn kết cao mà chúng tôi mong muốn. Những bối cảnh có giới hạn này, sau đó, tự cho mình là những ranh giới sáng tác cực kỳ tốt.
Như chúng ta đã thảo luận trong Chương 1, chúng ta có tùy chọn sử dụng các mô-đun trong một ranh giới quy trình để giữ các đoạn mã nguồn liên quan lại với nhau và cố gắng giảm sự ghép nối với các mô-đun khác trong hệ thống. Khi bạn bắt đầu trên một mã nguồn cơ sở mới, đây có là một khởi đầu tốt. Vì vậy, khi bạn đã tìm thấy các bối cảnh trong lĩnh vực của mình, hãy đảm bảo rằng chúng được mô hình hóa trong mã nguồn cơ sở của bạn dưới dạng mô-đun, với các mô hình được chia sẻ và ẩn.
Các ranh giới mô-đun này sau đó trở thành ứng viên xuất sắc cho các microservice. Nói chung, microservices cần phù hợp trong bối cảnh nhất định. Một khi bạn đã trở nên rất thành thạo, bạn có thể quyết định bỏ qua bước giữ cho bối cảnh có giới hạn được sử dụng như một mô-đun trong một hệ thống monolithic hơn và chuyển thẳng sang tạo ra một dịch vụ riêng biệt. Tuy nhiên, khi bắt đầu, hãy giữ một hệ thống mới ở khía cạnh gần giống monolithic hơn; việc sai lầm trong việc nhìn nhận ranh giới của các dịch vụ có thể gây hậu quả tốn kém, vì vậy hãy chờ đợi mọi thứ ổn định cho đến khi khi bạn nắm bắt được lĩnh vực mới, đó một là điều hợp lý. Chúng ta sẽ thảo luận thêm về vấn đề này trong Chương 5, cùng với các kỹ thuật giúp tách các hệ thống hiện có thành các microservice.
Vì vậy, nếu các ranh giới dịch vụ của chúng tôi phù hợp với các bối cảnh được giới hạn trong lĩnh vực của chúng tôi và microservices của chúng tôi đại diện cho các bối cảnh, chúng tôi đã có một khởi đầu tuyệt vời trong việc đảm bảo rằng các microservice của chúng tôi được liên kết lỏng lẻo và gắn kết chặt chẽ.
Phân rã sớm
Tại ThoughtWorks, bản thân chúng tôi đã trải qua những thách thức khi cố gắng phân chia các microservice quá nhanh. Bên cạnh việc tư vấn, chúng tôi cũng tạo ra một vài sản phẩm. Một trong số đó là SnapCI, một công cụ tích hợp liên tục (Continuous Integration) được cài đặt và phân phối liên tục (Continuous Delivery – Sau đây cũng viết là Continuous Delivery hoặc CD) (chúng ta sẽ thảo luận về các khái niệm đó sau trong Chương 6). Trước đây, nhóm đã làm việc trên một công cụ tương tự khác, Go-CD, một công cụ Continuous Delivery mã nguồn mở hiện có thể được deploy cục bộ thay vì được cài đạt trên nền tảng điện toán đám mây.
Mặc dù đã có một số mã nguồn được sử dụng lại từ rất sớm giữa các dự án SnapCI và Go-CD, nhưng cuối cùng SnapCI lại trở thành một mã nguồn cơ sở hoàn toàn mới. Tuy nhiên, kinh nghiệm trước đây của nhóm trong lĩnh vực công cụ CD khuyến khích họ tiến nhanh hơn trong việc xác định ranh giới và xây dựng hệ thống của họ như một tập hợp của các microservice.
Tuy nhiên, sau một vài tháng, rõ ràng là các trường hợp sử dụng của SnapCI đã khác nhau một cách tinh tế đến mức việc tiếp nhận các ranh giới dịch vụ ban đầu là không hoàn toàn đúng. Điều này dẫn đến nhiều thay đổi được thực hiện trên các dịch vụ và tất nhiên chi phí cho sự thay đổi cùng gia tăng. Cuối cùng, nhóm đã hợp nhất các dịch vụ trở lại thành một hệ thống monolithic, cho họ thời gian để hiểu rõ hơn về ranh giới nên tồn tại. Một năm sau, nhóm nghiên cứu đã có thể tách hệ thống monolithic thành các microservices, có ranh giới ổn định hơn nhiều. Đây không phải là ví dụ duy nhất về tình huống này mà tôi đã thấy. Việc phân ra sớm một hệ thống thành microservices có thể tốn kém, đặc biệt nếu bạn là người mới tham gia vào lĩnh vực. Theo nhiều cách, việc có một codebase tồn tại mà bạn muốn phân rã thành microservice dễ dàng hơn nhiều so với việc cố gắng xây dựng microservice ngay từ đầu.
Năng lực liên quan đến công việc kinh doanh
Khi bạn bắt đầu nghĩ về các bối cảnh và của chúng giới hạn tồn tại trong tổ chức của mình, bạn không nên nghĩ theo cách dữ liệu được chia sẻ, mà là về khả năng mà các bối cảnh đó cung cấp cho phần còn lại của lĩnh vực. Ví dụ, nhà kho có thể cung cấp khả năng có được danh sách hàng tồn kho hiện tại hoặc bối cảnh tài chính có thể hiển thị các tài khoản cuối tháng hoặc cho phép bạn thiết lập bảng lương cho một đợt tuyển dụng mới. Những khả năng này có thể yêu cầu trao đổi thông tin — các mô hình được chia sẻ — nhưng tôi đã thấy một cách quá thường xuyên việc suy nghĩ về dữ liệu dẫn đến các dịch vụ không có hoạt động gì liên quan đến việc hoạt động của công ty, và chỉ dựa trên các CRUD (tạo, đọc, cập nhật, xóa). Vì vậy, trước tiên hãy đặt hỏi "Bối cảnh này làm gì?", Sau đó là "Vậy nó cần dữ liệu gì để làm điều đó?"
Khi được mô hình hóa dưới dạng dịch vụ, các khả năng này trở thành các hoạt động chính sẽ được hiển thị qua một đường dây với các thành phần khác
Một vấn đề lặp lại vô tận
Nguyên văn: Turtles All the Way Down là một câu thể hiện một vấn đề lặp lại một cách vô tận. https://en.wikipedia.org/wiki/Turtles_all_the_way_down
Khi bắt đầu, bạn có thể sẽ xác định được các bối cảnh một cách chi tiết. Nhưng những bối cảnh này đến lượt nó có thể chứa những bối cảnh khác. Lấy ví dụ, bạn có thể phân chia bối cảnh của nhà kho thành các khả năng liên quan đến hoàn thành đơn đặt hàng, quản lý hàng tồn kho hoặc nhận hàng hóa. Khi xem xét ranh giới của các microservice, trước tiên hãy nghĩ đến các bối cảnh lớn hơn, ít chi tiết hơn, sau đó chia nhỏ theo các bối cảnh lồng nhau này khi bạn đang tìm kiếm lợi ích của việc tách các đường nối giữa chúng ra.
Tôi đã gặp các bối cảnh lồng nhau vẫn bị ẩn trong các bối cảnh khác, hãy kết hợp các microservice với nhau để tạo ra hiệu quả tuyệt vời. Đối với thế giới thực, họ vẫn đang sử dụng các năng lực mà nhà kho cung cấp, nhưng họ không biết rằng các yêu cầu của họ đang thực sự được ánh xạ một cách minh bạch tới hai hoặc nhiều dịch vụ riêng biệt, như bạn có thể thấy trong Hình 3-2. Đôi khi, bạn sẽ quyết định rằng sẽ hợp lý hơn khi bối cảnh giới hạn cấp cao hơn không được mô hình hóa một cách rõ ràng như một ranh giới dịch vụ, như trong Hình 3-3, vì vậy thay vì một ranh giới trong bối cảnh nhà kho duy nhất, thay vào đó bạn có thể chia nhỏ ra thành hàng tồn kho, thực hiện đơn đặt hàng, và nhận hàng hóa.
Hình 3-2. Microservices đại diện cho các bối cảnh được giới hạn lồng nhau ẩn bên trong nhà kho
Hình 3-3. Các bối cảnh giới hạn bên trong nhà kho được bật lên thành bối cảnh cấp cao nhất của riêng chúng
Nói chung, không có quy tắc nào quá nghiêm ngặt về cách tiếp cận nào phù hợp nhất. Tuy nhiên, việc bạn chọn cách tiếp cận lồng vào nhau hay tiếp cận theo cách tách biệt hoàn toàn phải dựa trên cơ cấu tổ chức của bạn. Nếu việc thực hiện đơn hàng, quản lý hàng tồn kho và nhận hàng được quản lý bởi các nhóm khác nhau, thì họ có thể nên được coi là microservice cấp cao nhất. Mặt khác, nếu tất cả chúng được quản lý bởi một nhóm, thì mô hình lồng nhau sẽ có ý nghĩa hơn. Điều này là do sự tác động lẫn nhau của cấu trúc tổ chức và kiến trúc phần mềm, mà chúng ta sẽ thảo luận ở phần cuối của cuốn sách trong Chương 10.
Một lý do khác để cách tiếp cận lồng nhau được ưa thích hơn có thể là chia nhỏ kiến trúc của bạn để đơn giản hóa việc kiểm thử. Ví dụ: khi kiểm thử các dịch vụ sử dụng dịch vụ kho hàng, tôi không phải phân tích từng dịch vụ bên trong bối cảnh của kho hàng, mà chỉ là nhưng API ít chi tiết hơn. Điều này cũng có thể cung cấp cho bạn một đơn vị cô lập khi xem xét các bài kiểm thử trên phạm vi lớn hơn. Ví dụ: tôi có thể quyết định thực hiện các kiểm thử từ đầu đến cuối trong đó tôi khởi chạy tất cả các dịch vụ bên trong bối cảnh kho hàng, nhưng đối với tất cả các service liên quan khác, tôi có thể bỏ qua chúng. Chúng ta sẽ khám phá thêm về kiểm thử và cách ly trong Chương 7.
Vấn đề giao tiếp trong các hoạt động của doanh nghiệp
Những thay đổi mà chúng tôi thực hiện đối với hệ thống của mình thường là những thay đổi mà doanh nghiệp muốn thực hiện về cách hệ thống hoạt động. Chúng tôi đang thay đổi chức năng — khả năng — được hiển thị cho khách hàng của chúng tôi. Nếu hệ thống của chúng tôi được phân tách theo các bối cảnh có giới hạn đại diện cho lĩnh vực của chúng tôi, thì những thay đổi chúng tôi muốn thực hiện có nhiều khả năng bị tách biệt trong một ranh giới microservice duy nhất. Điều này làm giảm số lượng điểm mà chúng tôi cần thực hiện thay đổi và cho phép chúng tôi deploy thay đổi đó một cách nhanh chóng.
Điều quan trọng nữa là phải nghĩ đến sự giao tiếp giữa các dịch vụ nhỏ này theo cùng một cách hiểu về hoạt động trong doanh nghiệp. Việc mô hình hóa phần mềm của bạn và sau đó là toàn bộ công việc kinh doanh không nên dừng lại ở ý tưởng về các bối cảnh có giới hạn. Các định nghĩa và ý tưởng giống nhau được chia sẻ giữa các bộ phận trong tổ chức của bạn phải được phản ánh trong các giao diện. Có thể hữu ích khi nghĩ về các biểu mẫu được gửi giữa các dịch vụ nhỏ này, giống như các biểu mẫu được trong cả tổ chức
Ranh giới về kỹ thuật
Việc xem xét những gì có thể xảy ra khi các dịch vụ được mô hình hóa không chính xác là việc làm hữu ích. Trước đây, tôi cùng một số đồng nghiệp đã làm việc với một khách hàng ở California, giúp công ty áp dụng một số phương pháp lập trình rõ ràng hơn và hướng tới tự động kiểm thử nhiều hơn. Chúng tôi đã bắt đầu với một số công việc đơn giản và có đã kết quả, chẳng hạn như phân tích dịch vụ, khi đó chúng tôi nhận thấy điều gì đó đáng lo ngại hơn nhiều. Tôi không thể đi quá chi tiết về những gì ứng dụng đã làm, nhưng đó là một ứng dụng công khai với lượng lớn khách hàng toàn cầu.
Đội ngũ và hệ thống đã ngày càng lớn mạnh. Ban đầu chỉ là tầm nhìn của một người, hệ thống đã sử dụng ngày càng nhiều tính năng và ngày càng có nhiều người dùng hơn. Cuối cùng, tổ chức quyết định nâng cao năng lực của nhóm bằng cách để một nhóm các developer mới có trụ sở tại Brazil đảm nhận một số công việc. Hệ thống được tách ra, với nửa phía trước của ứng dụng về cơ bản là stateless, deploy một trang web công khai, như trong Hình 3-4. Phần còn lại đơn giản là sử dụng kĩ thuật gọi thủ tục từ xa (RPC) trên một kho dữ liệu. Về cơ bản, hãy tưởng tượng bạn đã sử dụng một lớp kho lưu trữ (repository layer) trong mã nguồn cơ sở của mình và biến đây thành một dịch vụ riêng biệt.
Hình 3-4. Ranh giới dịch vụ được phân chia qua đường nối kỹ thuật
Các thay đổi thường xuyên phải được thực hiện cho cả hai dịch vụ. Cả hai dịch vụ đều nói chuyện với nhau qua các lệnh gọi phương thức kiểu RPC cấp thấp, mà kiểu kết nối này rất cứng nhắc nhưng dễ đứt gãy (chúng ta sẽ thảo luận về vấn đề này trong Chương 4). Giao diện dịch vụ cũng được sử dụng theo cách rất tùy tiện, dẫn đến vấn đề về hiệu suất. Điều này dẫn đến nhu cầu về các cơ chế xử lý RPC theo lô rất phức tạp. Tôi gọi đây là kiến trúc củ hành (onion architecture), vì nó có rất nhiều lớp và khiến tôi phải khóc khi phải cắt qua nó.
Bây giờ về mặt nổi của nó, ý tưởng tách hệ thống monolithic trước đây dưa trên các nhóm địa lý/tổ chức là hoàn toàn hợp lý, vì chúng tôi sẽ mở rộng trong Chương 10. Tuy nhiên, ở đây, thay vì thực hiện theo chiều dọc, tập trung vào các công việc của nghiệp vụ thông qua ngăn xếp, nhóm đã chọn những gì trước đây là đã được tạo ra thành API và tạo ra một lát cắt ngang.
Không phải lúc nào cũng đưa ra quyết định để lập mô hình ranh giới dịch vụ dọc theo các đường nối kỹ thuật cũng sai lầm. Tôi chắc chắn đã thấy điều này rất có ý nghĩa khi một tổ chức đang tìm cách đạt được các mục tiêu hiệu suất nhất định, chẳng hạn. Tuy nhiên, việc tìm kiếm các đường nối này chỉ là động lực phụ của bạn chứ không phải mục tiêu chính.
Tóm tắt
Trong chương này, bạn đã tìm hiểu một chút về điều gì tạo nên một dịch vụ tốt và cách tìm các đường nối trong không gian vấn đề của chúng tôi, mang lại cho chúng tôi lợi ích kép của cả khớp nối lỏng và tính liên kết cao. Các bối cảnh có giới hạn là một công cụ quan trọng giúp chúng ta tìm ra các đường nối này và bằng cách sắp xếp các microservice của chúng ta theo các ranh giới này, chúng ta cần đảm bảo rằng hệ thống cuối cùng có mọi cơ hội để giữ nguyên các tính năng đó. Chúng ta cũng đã có một gợi ý về cách chúng ta có thể chia nhỏ các microservice của mình hơn nữa, điều gì đó chúng ta sẽ khám phá sâu hơn sau. Và chúng ta cũng đã giới thiệu MusicCorp, lĩnh vực ví dụ mà chúng ta sẽ sử dụng trong suốt cuốn sách này.
Những ý tưởng được trình bày trong Thiết kế theo hướng lĩnh vực – DDD của Eric Evans rất hữu ích cho chúng ta trong việc tìm ra ranh giới hợp lý cho các dịch vụ và tôi mới chỉ sơ lược ở đây. Tôi giới thiệu cuốn sách Implementing Domain-Driven Design (Addison- Wesley) của Vaughn Vernon để giúp bạn hiểu tính thực tiễn của phương pháp này.
Mặc dù chương này chủ yếu là những thứ mang tính tổng quát, nhưng chúng ta cần đi sâu vào các kỹ thuật hơn trong các chương tiếp theo. Có rất nhiều cạm bẫy liên quan đến việc deploy giao diện giữa các dịch vụ, mà chúng có thể dẫn đến tất cả các loại rắc rối và chúng ta sẽ phải đi sâu vào chủ đề này nếu muốn giữ cho hệ thống của mình không trở thành một gã khổng lồ, rối rắm lộn xộn.
Như chúng ta đã thấy cho đến nay, microservice cung cấp cho chúng ta rất nhiều sự lựa chọn và theo đó là rất nhiều quyết định để đưa ra. Ví dụ, chúng ta nên sử dụng bao nhiêu công nghệ khác nhau, chúng ta nên để các nhóm khác nhau sử dụng các thành ngữ lập trình khác nhau, và chúng ta có nên tách hoặc hợp nhất một dịch vụ không? Làm thế nào để chúng ta đưa ra những quyết định này? Với tốc độ thay đổi nhanh hơn và môi trường linh hoạt hơn mà các kiến trúc này cho phép, vai trò của kiến trúc sư của hệ thống cũng phải thay đổi. Trong chương này, tôi sẽ có một cái nhìn khá chắc chắn về vai trò của một kiến trúc sư của hệ thống là gì và hy vọng sẽ khởi động một cuộc tấn công cuối cùng vào toà tháp ngà voi (ám chỉ việc xa rời thực tế)
Việc so sánh khập khiễng
Bạn tiếp tục sử dụng từ đó. Nhưng tôi không nghĩ rằng nó có nghĩa là những gì bạn nghĩ nó.
— Inigo Montoya, từ Cô dâu công chúa
Kiến trúc sư của hệ thống là một công việc quan trọng. Họ chịu trách nhiệm đảm bảo rằng chúng ta có một tầm nhìn kỹ thuật hợp nhất, một tầm nhìn sẽ giúp hệ thống của chúng tôi có thể đáp ứng được nhu cầu của khách hàng. Ở một số công ty, họ có thể chỉ phải làm việc với một nhóm, trong trường hợp đó, vai trò của họ và trưởng nhóm kỹ thuật thường giống nhau. Ở những người khác, họ có thể xác định tầm nhìn cho toàn bộ chương trình làm việc, phối hợp với nhiều nhóm trên toàn thế giới, hoặc thậm chí có thể là toàn bộ tổ chức. Ở bất kỳ cấp độ nào họ hoạt động, vai trò này rất khó để xác định rõ ràng, và mặc dù nó thường là một bước tiến nghề nghiệp rõ ràng cho developer trong các tổ chức doanh nghiệp, nó cũng là một vai trò nhận được nhiều chỉ trích hơn hầu hết vai trò bất kỳ nào khác. Hơn bất kỳ vai trò nào khác, họ có thể có tác động trực tiếp đến chất lượng của các hệ thống được xây dựng, đến điều kiện làm việc của đồng nghiệp và khả năng của tổ chức của họ để đáp ứng với sự thay đổi, nhưng dường như chúng ta thường làm sai vài trò của công việc này. Tại sao vậy?
Ngành công nghiệp (phần mềm) của chúng ta là một ngành trẻ. Đây là điều mà chúng ta dường như đã quên, nhưng chúng ta chỉ tạo ra các chương trình chạy trên thứ mà chúng ta gọi là máy tính trong khoảng 70 năm trở lại đây. Vì vậy, chúng ta không ngừng tìm kiếm một thứ tương tự ở các ngành nghề khác để cố gắng giải thích những gì chúng ta đang làm. Chúng ta không phải là bác sĩ hay kỹ sư y tế, nhưng cũng không phải là thợ sửa ống nước hay thợ điện. Thay vào đó, chúng ta thường nằm ở lưng chừng, điều này khiến xã hội khó hiểu chúng ta, hoặc chúng ta không hiểu chúng ta đang ở đâu.
Vì vậy, chúng ta vay mượn từ các ngành nghề khác. Chúng tôi tự gọi mình là “kỹ sư” phần mềm, hoặc"Kiến trúc sư." Nhưng chúng ta không phải kiến trúc sư, nhỉ? Kiến trúc sư và kỹ sư (ở ngành khác) có một sự nghiêm khắc và tinh tế mà chúng ta chỉ có thể mơ ước, và tầm quan trọng của họ trong xã hội được hiểu rõ. Tôi nhớ đã nói chuyện với một người bạn của tôi, một ngày trước khi anh ta trở thành một kiến trúc sư có trình độ. “Ngày mai,” anh ấy nói, "nếu tôi cho bạn lời khuyên ở quán rượu về cách xây dựng một thứ gì đó và điều đó là sai, tôi sẽ phải chịu trách nhiệm. Tôi có thể bị kiện, vì theo pháp luật, tôi bây giờ là một kiến trúc sư có năng lực và tôi phải chịu trách nhiệm nếu tôi làm sai. " Tầm quan trọng của những công việc này đối với xã hội có nghĩa là cần phải có những bằng cấp cần thiết để đáp ứng. Ví dụ, ở Anh, bạn phải học tối thiểu bảy năm trước khi được gọi là kiến trúc sư. Nhưng những công việc này cũng dựa trên một lượng kiến thức có từ hàng nghìn năm trước. Và chúng ta? Không hẳn. Đó cũng là lý do tại sao tôi xem hầu hết các hình thức chứng chỉ CNTT là vô giá trị, vì chúng hầu như không thể khẳng định chúng ta tốt như thế nào.
Một phần trong chúng ta có nhu cầu được công nhận, vì vậy chúng tôi mượn tên từ những ngành nghề khác đã có được sự công nhận mà chúng tôi là một ngành khao khát. Nhưng điều này có thể gây hại gấp đôi. Đầu tiên, nó ngụ ý rằng chúng ta biết mình đang làm gì, khi nào thì rõ ràng là không. Tôi sẽ không nói rằng các công trình xây dựng và cầu nối không bao giờ sụp đổ, nhưng tỉ lệ ít hơn nhiều so với số lần các chương trình của chúng ta sẽ gặp sự cố, khiến cho việc so sánh với các kỹ sư là khập khiễng. Thứ hai, các phép loại suy bị chia nhỏ rất nhanh khi chỉ nhìn lướt qua. Để xoay chuyển tình thế, nếu việc xây dựng cây cầu giống như một chương trình, chúng tôi sẽ phát hiện ra rằng bờ biển xa bây giờ đã xa hơn 50 mét, rằng nó thực sự là bùn chứ không phải đá granit, và thay vì xây dựng một cây cầu mà đáng lẽ chúng tôi sẽ xây dựng – thay vào đó là một cây cầu đường bộ. Phần mềm của chúng ta không bị ràng buộc bởi các quy tắc vật lý giống như các kiến trúc sư hoặc kỹ sư thực sự phải đáp ứng và những gì chúng tôi tạo ra được thiết kế để linh hoạt, thích ứng và phát triển theo yêu cầu của người dùng.
Có lẽ thuật ngữ kiến trúc sư(Solution Architect hay Software Architect) đã gây hại nhiều nhất. Ý tưởng về một người lập kế hoạch chi tiết cho người khác giải thích và mong muốn điều này được thực hiện. Một sự cân bằng giữa một nghệ sỹ và một kỹ sư, giám sát việc tạo của những thứ tông thường với một tầm nhìn duy nhất, với tất cả các quan điểm khác là không phù hợp, ngoại trừ sự phản đối không thường xuyên từ kỹ sưvề các quy luật vật lý. Trong ngành của chúng ta, quan điểm này của kiến trúc sư của hệ thống dẫn đến một số thực hành khủng khiếp. Sơ đồ này đến sơ đồ khác, trang này qua trang khác của tài liệu, được tạo ra với mục đích cung cấp thông tin về việc xây dựng một hệ thống hoàn hảo, mà không tính đến tương lai về cơ bản không thể biết trước được. Hoàn toàn không có bất kỳ hiểu biết nào về mức độ khó thực hiện, hoặc chỉ là nó có thực sự hoạt động hay không, hãy để một mình có khả năng thay đổi khi chúng ta có hiểu biết nhiều hơn.
Khi chúng ta so sánh mình với các kỹ sư hoặc kiến trúc sư, chúng ta có nguy cơ khiến mọi người trở thành kẻ phá hoại. Thật không may, chúng tôi đang mắc kẹt với từ kiến trúc sư cho đến bây giờ. Vì vậy, điều tốt nhất chúng ta có thể làm là xác định lại ý nghĩa của nó trong ngữ cảnh của chúng ta.
Một tầm nhìn tiến hóa cho kiến trúc sư
Các yêu cầu của chúng tôi thay đổi nhanh hơn so với những người thiết kế và xây dựng các tòa nhà — các công cụ và kỹ thuật theo ý của chúng tôi cũng vậy. Những thứ chúng ta tạo ra không phải là những điểm cố định trong thời gian. Sau khi được đưa vào môi trường production, phần mềm của chúng tôi sẽ tiếp tục phát triển khi cách thức sử dụng thay đổi. Đối với hầu hết những thứ chúng tôi tạo ra, chúng tôi phải chấp nhận rằng một khi phần mềm đến tay khách hàng, chúng tôi sẽ phải phản ứng và thích nghi, thay vì nó là một sản phẩm không bao giờ thay đổi. Do đó, các kiến trúc sư của chúng tôi cần phải thay đổi suy nghĩ của họ thay vì việc tạo ra sản phẩm cuối cùng hoàn hảo, mà tập trung vào việc giúp tạo ra một framework trong đó các hệ thống phù hợp có thể xuất hiện và tiếp tục phát triển khi chúng tôi có hiểu biết nhiều hơn.
Mặc dù cho đến nay, tôi đã dành phần lớn thời lượng của chương để cảnh báo bạn không nên so sánh bản thân của chúng ta quá nhiều với các ngành nghề khác, nhưng có một điểm tương đồng mà tôi thích khi nói đến vai trò của kiến trúc sư của hệ thống CNTT và tôi nghĩ tốt hơn nên gói gọn những gì chúng ta muốn. vai trò hiện hữu. Erik Doernenburg lần đầu tiên chia sẻ với tôi ý tưởng rằng chúng ta nên nghĩ về vai trò của mình với tư cách là nhà quy hoạch thành phố hơn là kiến trúc sư cho môi trường xây dựng. Vai trò của người lập kế hoạch thị trấn hẳn đã quen thuộc với bất kỳ bạn nào đã chơi SimCity trước đây. Vai trò của người lập quy hoạch thành phố là xem xét vô số nguồn thông tin và sau đó cố gắng tối ưu hóa bố cục của thành phố để phù hợp nhất với nhu cầu của người dân hiện nay, có tính đến việc sử dụng trong tương lai. Tuy nhiên, cách anh ấy ảnh hưởng đến cách thành phố phát triển là rất thú vị. Anh ta không nói, “xây dựng tòa nhà cụ thể này ở đó”; thay vào đó, anh ta tạo ra các vùng trong**thành phố. Vì vậy, như ở SimCity, bạn có thể chỉ định một phần thành phố của mình là khu công nghiệp và một phần khác là khu dân cư. Sau đó, những người khác sẽ quyết định những tòa nhà chính xác được tạo ra, nhưng có những hạn chế: nếu bạn muốn xây dựng một nhà máy, nó sẽ cần phải ở trong một khu công nghiệp. Thay vì lo lắng quá nhiều về những gì xảy ra trong một khu vực, thay vào đó, người lập kế hoạch thành phố sẽ dành nhiều thời gian hơn để tìm hiểu cách thức di chuyển của mọi người và các tiện ích từ khu vực này sang khu vực khác.
Nhiều người đã ví một thành phố như một sinh vật sống. Thành phố thay đổi theo thời gian. Nó thay đổi và phát triển khi những cư dân sử dụng nó theo những cách khác nhau, hoặc khi các lực lượng bên ngoài định hình nó. Người quy hoạch thành phố cố gắng hết sức để dự đoán những thay đổi này, nhưng chấp nhận rằng việc cố gắng kiểm soát trực tiếp tất cả các khía cạnh của những gì xảy ra là vô nghĩa.
Việc so sánh với phần mềm nên rõ ràng. Khi người dùng sử dụng phần mềm của chúng tôi, chúng tôi cần phản ứng và thay đổi. Chúng ta không thể lường trước mọi điều sẽ xảy ra, và vì vậy thay vì lập kế hoạch cho bất kỳ trường hợp nào, chúng ta nên lên kế hoạch cho phép thay đổi bằng cách tránh việc tạo ra một điều cuối cùng không bao giờ thay đổi. Thành phố của chúng ta — hệ thống — cần phải là một nơi tốt đẹp, hạnh phúc cho tất cả những ai sử dụng nó. Một điều mà mọi người thường quên là hệ thống của chúng tôi không chỉ đáp ứng người dùng; nó cũng tạo điều kiện cho các developer và nhà kinh doanh, những người cũng phải làm việc ở đó và những người có công việc đảm bảo rằng nó có thể thay đổi theo yêu cầu. Để mượn một thuật ngữ từ Frank Buschmann, các kiến trúc sư của hệ thống có nhiệm vụ đảm bảo rằng hệ thống cũng có thể sử dụng được cho các developer.
Một nhà quy hoạch, cũng giống như một kiến trúc sư, cũng cần biết khi nào kế hoạch của mình không được sử dụng hoặc tuân thủ. Vì anh ta có ít chỉ thị hơn, nên số lần anh ta cần tham gia để chỉ ra sự đúng sai là ít nhất, nhưng nếu ai đó quyết định xây dựng một nhà máy xử lý nước thải trong khu dân cư, anh ta cần phải có khả năng đóng cửa nó.
Vì vậy, các kiến trúc sư của hệ thống của chúng tôi với tư cách là những nhà quy hoạch thị trấn cần phải định hướng theo hướng tổng quan và chỉ tham gia vào việc cụ thể hóa chi tiết thực hiện trong một số trường hợp cụ thể. Họ cần đảm bảo rằng hệ thống phù hợp với mục đích ngay bây giờ, nhưng cũng là một nền tảng cho tương lai. Và họ cần đảm bảo rằng đó là một hệ thống khiến người dùng và developer hài lòng như nhau. Điều này nghe có vẻ như một yêu cầu khá cao. Vậy, chúng ta bắt đầu từ đâu?
Phân vùng
Vì vậy, tiếp tục ẩn dụ về kiến trúc sư của hệ thống là người quy hoạch thành phố thêm một chút nữa, chúng ta có những khu vực nào? Đây là các ranh giới dịch vụ của chúng tôi hoặc có thể là danh sách các dịch vụ được nhóm lại một cách chi tiết. Là kiến trúc sư, chúng ta cần ít để ý về những gì xảy ra bên trong từng khu vực hơn là những gì xảy ra giữa các khu vực với nhau. Điều đó có nghĩa là chúng tôi cần dành thời gian suy nghĩ về cách các dịch vụ của chúng tôi nói chuyện với nhau hoặc đảm bảo rằng chúng tôi có thể duy trì tình trạng hoạt động tổng thể của hệ thống một cách chính xác. Mức độ tham gia của chúng tôi vào bên trong khu vực sẽ khác nhau
một phần nào đó. Nhiều tổ chức đã áp dụng microservices để tối đa hóa quyền tự chủ của các nhóm, điều mà chúng tôi sẽ mở rộng trong Chương 10. Nếu bạn ở trong một tổ chức như vậy, bạn sẽ dựa nhiều hơn vào nhóm đó để đưa ra quyết định phù hợp tại từng chỗ.
Nhưng giữa các khu vực, hoặc các ô trên bản đồ kiến trúc truyền thống của chúng tôi, chúng tôi cần phải cẩn thận; làm sai ở đây dẫn đến tất cả các loại vấn đề và có thể rất khó sửa chữa.
Trong mỗi dịch vụ, bạn có thể đồng ý với nhóm sở hữu khu vực đó chọn một kho lưu trữ dữ liệu hoặc công nghệ khác nhau. Tất nhiên, những mối quan tâm khác có thể xuất hiện ở đây. Xu hướng để các nhóm chọn công cụ phù hợp cho công việc của bạn có thể bị hạn chế bởi thực tế là việc thuê người hoặc di chuyển họ giữa các nhóm trở nên khó khăn hơn nếu bạn có 10 ngăn xếp công nghệ khác nhau để hỗ trợ. Tương tự, nếu mỗi đội chọn một kho dữ liệu hoàn toàn khác nhau, bạn có thể thấy mình thiếu đủ kinh nghiệm để chạy bất kỳ kho dữ liệu nào trong số họ trên quy mô lớn. Netflix, ví dụ, hầu hết đã chuẩn hóa và sử dụng Cassandra như một kho lưu trữ dữ liệu. Mặc dù nó có thể không hẳn đã là công nghệ phù hợp nhất cho tất cả các trường hợp, nhưng Netflix cảm thấy rằng giá trị thu được bằng cách xây dựng công cụ và kiến thức chuyên môn xung quanh Cassandra quan trọng hơn việc phải hỗ trợ và vận hành trên quy mô nhiều nền tảng khác có thể phù hợp hơn cho từng các nhiệm vụ nhất định. Netflix là một ví dụ điển hình, trong đó quy mô có thể là yếu tố mang tính quyết định mạnh nhất, bạn có thể thấy ý tưởng đó khá rõ ràng.
Tuy nhiên, phần giữa các dịch vụ với nhau mới là nơi mọi thứ có thể trở nên lộn xộn. Nếu một dịch vụ quyết định cung cấp REST thông qua HTTP, một dịch vụ khác sử dụng giao thức bộ đệm và một dịch vụ thứ ba sử dụng Java RMI, thì việc tích hợp chúng có thể trở thành một cơn ác mộng vì các dịch vụ sử dụng chúng phải chịu và hỗ trợ nhiều kiểu giao thức. Đây là lý do tại sao tôi cố gắng bám sát tôn chỉ rằng chúng ta nên “lo lắng về những gì xảy ra giữa các hộp và tự do trong những gì xảy ra bên trong.”
Kiến trúc mã nguồn
Nếu chúng tôi muốn đảm bảo rằng các hệ thống chúng tôi tạo ra có thể sử dụng được cho các developer của chúng tôi, thì các kiến trúc sư của chúng tôi cần phải hiểu tác động của các quyết định của họ. Ít nhất, điều này có nghĩa là dành thời gian cho nhóm và lý tưởng là nó có nghĩa là những developer này cũng thực sự dành thời gian viết mã nguồn cho nhóm. Đối với những bạn thực hành phát triển theo cặp**(pair-programing),** việc một kiến trúc sư tham gia nhóm trong một thời gian ngắn với tư cách là một thành viên của cặp sẽ trở thành một vấn đề đơn giản. Tốt nhất, bạn nên làm những câu chuyện bình thường, để thực sự hiểu công việc bình thường là như thế nào. Tôi không thể nhấn mạnh tầm quan trọng của kiến trúc sư khi làm việc cùng nhóm! Điều này hiệu quả hơn đáng kể so với việc gọi điện hoặc chỉ nhìn vào mã nguồn của cô ấy.
Về mức độ thường xuyên mà bạn nên làm điều này, điều đó phụ thuộc rất nhiều vào quy mô của (các) nhóm mà bạn đang làm việc. Nhưng điều quan trọng là nó phải là một hoạt động thường xuyên. Ví dụ: nếu bạn đang làm việc với bốn nhóm, dành nửa ngày cho mỗi nhóm bốn tuần một lần đảm bảo bạn xây dựng nhận thức và cải thiện giao tiếp với các nhóm mà bạn đang làm việc.
Phương pháp tiếp cận có nguyên tắc
Các quy tắc dành cho sự vâng lời của những kẻ ngu ngốc và sự hướng dẫn của những nhà thông thái.
— Thường được cho là của Douglas Bader
Việc đưa ra quyết định trong thiết kế hệ thống, tất cả chỉ xoay quanh sự đánh đổi và microservice mang đến cho chúng ta rất nhiều đánh đổi! Khi chọn một kho dữ liệu, chúng ta có chọn một nền tảng mà chúng ta có ít kinh nghiệm, nhưng điều đó mang lại cho chúng ta khả năng mở rộng tốt hơn không? Chúng tôi có thể sử dụng hai stack công nghệ khác nhau trong hệ thống của mình không? Vậy nếu sử dụng ba thì sao? Một số quyết định có thể được thực hiện hoàn toàn ngay tại chỗ với thông tin có sẵn cho chúng tôi, và đây là những thứ dễ thực hiện nhất. Nhưng những quyết định có thể phải được thực hiện trên thông tin không đầy đủ thì sao?
Việc lập khung ở đây có thể hữu ích và một cách tuyệt vời để giúp lập khung cho việc ra quyết định của chúng ta là xác định một bộ các nguyên tắc và thực hành hướng dẫn nó, dựa trên các mục tiêu mà chúng ta đang cố gắng đạt được. Chúng ta hãy xem xét từng thứ một.
Mục tiêu chiến lược
Vai trò của kiến trúc sư đã đủ khó khăn, vì vậy, may mắn là chúng tôi thường không phải xác định các mục tiêu chiến lược! Các mục tiêu chiến lược phải nói lên được vị trí của công ty và cách công ty tự thấy là tốt nhất để làm cho khách hàng hài lòng. Đây sẽ là những mục tiêu cấp cao và có thể hoàn toàn không bao gồm công nghệ. Chúng có thể được xác định ở cấp độ công ty hoặc cấp độ phòng ban. Chúng có thể là những thứ như “Mở rộng sang Đông Nam Á để mở khóa các thị trường mới” hoặc “Hãy để khách hàng đạt được càng nhiều càng tốt bằng cách sử dụng các dịch vụ tự phục vụ.” Điều quan trọng là đây là nơi tổ chức của bạn đứng đầu, vì vậy bạn cần đảm bảo công nghệ phù hợp với nó.
Nếu bạn là người xác định tầm nhìn kỹ thuật của công ty, điều này có thể có nghĩa là bạn sẽ cần dành nhiều thời gian hơn cho các bộ phận phi kỹ thuật trong tổ chức của mình (hoặc bộ phận kinh doanh, như chúng thường được gọi). Tầm nhìn thúc đẩy cho doanh nghiệp là gì? Và nó thay đổi như thế nào?
Nguyên tắc
Nguyên tắc là những quy tắc bạn đã thực hiện để điều chỉnh những gì bạn đang làm với một số mục tiêu lớn hơn và đôi khi sẽ thay đổi. Ví dụ: nếu một trong những mục tiêu chiến lược của bạn với tư cách là một tổ chức là giảm thời gian triển khai các tính năng mới, bạn có thể xác định một nguyên tắc nói rằng nhóm delivery có toàn quyền kiểm soát vòng đời của phần mềm của họ để deliver bất cứ khi nào họ sẵn sàng, độc lập với bất kỳ đội nào khác. Nếu một mục tiêu khác là tổ chức của bạn là dịch chuyển mạnh mẽ sang việc triển khai dịch vụ của mình ở các quốc gia khác, bạn có thể quyết định thực hiện một nguyên tắc rằng toàn bộ hệ thống phải có tính di động để cho phép nó được deploy tại các quốc gia đó nhằm tôn trọng chủ quyền của dữ liệu.
Bạn hoàn toàn không muốn có vô số nguyên tắc. Ít hơn 10 là một con số tốt – đủ nhỏ để mọi người có thể nhớ chúng hoặc để phù hợp với các áp phích nhỏ. Bạn càng có nhiều nguyên tắc, thì khả năng chúng trùng lặp hoặc mâu thuẫn với nhau càng lớn.
12 Factors của Heroku là một tập hợp các nguyên tắc thiết kế có cấu trúc xoay quanh mục tiêu giúp bạn tạo ra các ứng dụng hoạt động tốt trên nền tảng Heroku. Chúng cũng có thể có ý nghĩa trong các ngữ cảnh khác. Một số nguyên tắc thực sự là những ràng buộc dựa trên các hành vi mà ứng dụng của bạn cần thể hiện để hoạt động trên Heroku. Ràng buộc thực sự là một thứ rất khó (hoặc hầu như không thể) thay đổi, trong khi các nguyên tắc là thứ chúng ta quyết định lựa chọn. Bạn có thể quyết định gọi rõ ràng những điều đó là nguyên tắc so với những điều là ràng buộc, để giúp chỉ ra những điều bạn thực sự không thể thay đổi. Cá nhân tôi nghĩ rằng có thể có một số giá trị trong việc giữ chúng trong cùng một danh sách để khuyến khích những ràng buộc đầy thử thách thỉnh thoảng và xem liệu chúng có thực sự bất di bất dịch hay không!
Thực hành
Thực hành của chúng tôi là cách chúng tôi đảm bảo các nguyên tắc của chúng tôi đang được thực hiện. Chúng là một tập hợp các hướng dẫn chi tiết, thiết thực để thực hiện các nhiệm vụ. Chúng thường sẽ là công nghệ cụ thể, và phải đủ đơn giản để bất kỳ developer nào cũng có thể hiểu được chúng. Các phương pháp thực hành có thể bao gồm hướng dẫn mã hóa, thực tế là tất cả dữ liệu log cần được quản lý tập trung hoặc HTTP / REST là kiểu tích hợp tiêu chuẩn. Do bản chất kỹ thuật của chúng, các thực hành thường sẽ thay đổi thường xuyên hơn các nguyên tắc.
Cũng như các nguyên tắc, đôi khi thực hành phản ánh những hạn chế trong tổ chức của bạn. Ví dụ: nếu bạn chỉ hỗ trợ CentOS, điều này sẽ cần được phản ánh trong thực hành của bạn.
Thực hành nên làm nền tảng cho các nguyên tắc của chúng tôi. Một nguyên tắc nêu rõ rằng các nhóm deliver vận hành toàn bộ vòng đời của hệ thống của họ có thể có nghĩa là bạn có một thông lệ cho rằng tất cả các dịch vụ được deploy vào các tài khoản AWS riêng biệt, cung cấp khả năng tự quản lý tài nguyên và cách ly khỏi các nhóm khác.
Kết hợp các nguyên tắc và thực hành
Nguyên tắc của một người là thực hành của người khác. Ví dụ, bạn có thể quyết định gọi việc sử dụng HTTP/REST là một nguyên tắc hơn là một thực hành. Và điều đó sẽ ổn thôi. Điểm mấu chốt là giá trị khi có những ý tưởng bao quát hướng dẫn cách hệ thống phát triển và đủ chi tiết để mọi người biết cách thực hiện những ý tưởng đó. Đối với một nhóm đủ nhỏ, có lẽ là một nhóm duy nhất, việc kết hợp các nguyên tắc và thực hành có thể ổn. Tuy nhiên, đối với các tổ chức lớn hơn, nơi công nghệ và phương thức làm việc có thể khác nhau, bạn có thể muốn có một bộ thực hành khác ở những nơi chỗ khác nhau, miễn là cả hai đều hướng tới một bộ nguyên tắc chung. Ví dụ, một nhóm .NET có thể có một bộthực hành và một nhóm Java khác, với một bộ thực hành chung cho cả hai. Tuy nhiên, các nguyên tắc có thể giống nhau cho cả hai.
Một ví dụ trong thế giới thực
Đồng nghiệp của tôi, Evan Bottcher, đã phát triển sơ đồ thể hiện trong Hình 2-1 trong quá trình làm việc với một trong những khách hàng của chúng tôi. Hình này cho thấy sự tác động lẫn nhau của các mục tiêu, nguyên tắc và thực hành theo một định dạng rất rõ ràng. Trong vòng một vài năm, các hoạt động ở ngoài cùng bên phải sẽ thay đổi khá thường xuyên, trong khi các nguyên tắc vẫn khá tĩnh. Một sơ đồ như thế này có thể được in một cách độc đáo trên một tờ giấy và được chia sẻ, và mỗi ý tưởng đủ đơn giản để các developer trung bình có thể ghi nhớ. Tất nhiên, có nhiều chi tiết hơn đằng sau mỗi điểm ở đây, nhưng có thể trình bày rõ điều này ở dạng tổng hợp là rất hữu ích.
Hình 2-1. Một ví dụ thực tế về các nguyên tắc và thực hành
Có lý do để có tài liệu hỗ trợ một số mục này. Tuy nhiên, về cơ bản, tôi thích ý tưởng có một bộ mã nguồn mẫu mà bạn có thể xem, kiểm tra và chạy, là hiện thân của những ý tưởng này. Thậm chí tốt hơn, chúng ta có thể tạo ra công cụ làm đúng việc. Chúng ta sẽ thảo luận sâu hơn về vấn đề đó trong giây lát.
Tiêu chuẩn bắt buộc
Khi bạn đang nghiên cứu các phương pháp của mình và suy nghĩ về những đánh đổi mà bạn cần thực hiện, một trong những điểm cân bằng cốt lõi cần tìm là mức độ biến thiên cho phép trong hệ thống của bạn. Một trong những cách quan trọng để xác định những gì nên không đổi từ dịch vụ này sang dịch vụ khác là xác định một dịch vụ tốt, hoạt động tốt trông như thế nào. Dịch vụ “tốt” trong hệ thống của bạn là gì? Nó cần có những khả năng nào để đảm bảo rằng hệ thống của bạn có thể quản lý được và một dịch vụ không tốt sẽ không làm hỏng toàn bộ hệ thống? Và, cũng như với mọi người, những gì một công dân tốt trong một bối cảnh không phản ánh những gì nó trông như thế nào ở một nơi khác. Tuy nhiên, có một số đặc điểm chung của các dịch vụ hoạt động tốt mà tôi nghĩ là khá quan trọng để quan sát. Đây là một số lĩnh vực chính mà việc mọi thứ có quá nhiều khác biệt có thể dẫn đến một thời kì cực kì khó khăn. Như Ben Christensen từ Netflix đã nói, khi chúng ta nghĩ về bức tranh lớn hơn, “nó cần phải là một hệ thống gắn kết được tạo thành từ nhiều bộ phận nhỏ có vòng đời tự trị nhưng tất cả lại kết hợp với nhau”. Vì vậy, chúng tôi cần tìm sự cân bằng giữa việc tối ưu hóa choquyền tự chủ của từng microservice mà không làm hỏngbức tranh toàn cảnh. Xác định các thuộc tính rõ ràng mà mỗi dịch vụ phải có là một cách để xác định rõ ràng sự cân bằng đó nằm ở đâu.
Giám sát
Điều cốt lõiở đây là chúng tôi có thể vẽ ra các quan điểm nhất quán, tầm nhìn xuyên suốt các service về tình trạng sức khoẻ của hệ thống. Đây phải là cái nhìn toàn hệ thống, không phải là từng dịch vụ đơn lẻ. Như chúng ta sẽ thảo luận trong Chương 8, việc biết tình trạng của từng dịch vụ là hữu ích, nhưng thường chỉ khi bạn đang cố gắng chẩn đoán một vấn đề rộng hơn hoặc hiểu một xu hướng lớn hơn. Để làm cho điều này dễ dàng nhất có thể, tôi khuyên bạn nên đảm bảo rằng tất cả các dịch vụ cần đưa ra các chỉ số liên quan đến sức khỏe và giám sát chung theo cùng một cách.
Bạn có thể chọn áp dụng cơ chế đẩy (push), trong đó mỗi dịch vụ cần đẩy dữ liệu này vào một trung tâm xử lý. Đối với các chỉ số, đây có thể là Graphite, và đối với sức khoẻ của hệ thống, nó có thể là Nagios. Hoặc bạn có thể quyết định sử dụng hệ thống polling để lấy dữ liệu từ chính các nút. Nhưng bất cứ điều gì bạn chọn, hãy cố gắng giữ cho nó được chuẩn hóa. Làm cho công nghệ bên trong hộp trở nên mờ đục và không yêu cầu hệ thống giám sát của bạn phải thay đổi để hỗ trợ nó. Có một yêu cầu ở đây: log cần đặt tập trung ở một chỗ
Giao diện – Giao thức
Chọn một số lượng nhỏ các công nghệ giao diện đã xác định sẽ giúp tích hợp những người dùng mới. Một là một con số tốt khi nói về số lượng tiêu chuẩn. Hai cũng không quá tệ. Có 20 kiểu tích hợp khác nhau thì không tốt. Đây không chỉ là việc chọn công nghệ và giao thức. Ví dụ: nếu bạn chọn HTTP/REST, bạn sẽ sử dụng động từ hay danh từ? Bạn sẽ xử lý việc phân trang tài nguyên như thế nào? Bạn sẽ xử lý việc lập phiên bản của các điểm cuối như thế nào?
An toàn về mặt kiến trúc
Chúng tôi không thể để một dịch vụ có hành vi xấu làm hỏng bữa tiệc của tất cả mọi người. Chúng tôi phải đảm bảo rằng các dịch vụ của chúng tôi bảo vệ bản thân chúng khỏi nhưng vấn đề như downtime hay không thể gọi đến dịch vụ. Chúng ta càng có nhiều dịch vụ mà không xử lý đúng về khả năng thất bại của các dịch vụ khác khi gọi đến, thì hệ thống của chúng ta sẽ càng trở nên mong manh hơn. Điều này có nghĩa là bạn có thể sẽ muốn bắt buộc tối thiểu mỗi dịch vụ mà gọi đến dịch vụ khác phải có connection pool riêng và bạn thậm chí có thể đi xa hơn khi nói rằng mỗi dịch vụ cũng sử dụng một bộ ngắt mạch (circuit breaker). Điều này sẽ được đề cập sâu hơn khi chúng ta thảo luận về microservices ở quy mô lớn trong Chương 11.
Chơi theo luật cũng quan trọng khi nói đến mã phản hồi (response code). Nếu bộ ngắt mạch của bạn dựa vào mã HTTP và một dịch vụ quyết định gửi lại mã 2XX do lỗi hoặc nhầm lẫn mã 4XX với mã 5XX, thì các biện pháp an toàn này có thể bị ảnh hưởng. Các mối quan tâm tương tự sẽ áp dụng ngay cả khi bạn không sử dụng HTTP; hiểu sự khác biệt giữa một yêu cầu OK và được xử lý chính xác, một yêu cầu không tốt và ngăn dịch vụ làm bất cứ điều gì với nó và một yêu cầu có thể OK nhưng chúng tôi không thể biết được vì máy chủ không hoạt động là chìa khóa để đảm bảo chúng tôi có thể thất bại nhanh chóng và theo dõi các vấn đề. Nếu các dịch vụ của chúng tôi hoạt động nhanh và lỏng lẻo với các quy tắc này, chúng tôi sẽ dẫn đến một hệ thống dễ bị tấn công hơn.
Quản trị thông qua quy tắc
Cùng nhau và thống nhất về cách mọi thứ có thể được thực hiện là một ý kiến hay. Nhưng dành thời gian để đảm bảo rằng mọi người đang tuân theo các nguyên tắc này sẽ kém thú vị hơn, vì đang đặt gánh nặng lên các developer trong việc deploy tất cả những điều tiêu chuẩn này mà bạn mong đợi mỗi dịch vụ thực hiện. Tôi rất tin tưởng vào việc giúp bạn dễ dàng làm điều đúng đắn. Hai kỹ thuật mà tôi thấy hoạt động tốt ở đây là sử dụng các mẫu và cung cấp các khuôn mẫu dịch vụ.
Người làm mẫu
Viết tài liệu là tốt và hữu ích. Tôi thấy rõ giá trị, nên sau tất cả tôi đã viết cuốn sách này. Nhưng các developer cũng thích viết mã nguồn, và mã nguồn là thứ mà họ có thể chạy và khám phá. Nếu bạn có một bộ tiêu chuẩn hoặc phương pháp hay nhất mà bạn muốn khuyến khích, thì việc có những ví dụ mẫu mà bạn có thể chỉ cho mọi người sẽ hữu ích. Ý tưởng là mọi người không thể sai lầm chỉ bằng cách bắt chước một số bộ phận tốt hơn trong hệ thống của bạn.
Lý tưởng nhất, đây phải là những dịch vụ trong thế giới thực mà bạn có để làm mọi thứ ổn thỏa, chứ không phải là những dịch vụ biệt lập chỉ được deploy để trở thành những ví dụ hoàn hảo. Bằng cách đảm bảo rằng những ví dụ mẫu của bạn thực sự đang được sử dụng, bạn đảm bảo rằng tất cả các nguyên tắc bạn thực hiện thực sự có ý nghĩa.
Một dịch vụ mẫu phù hợp
Sẽ thật tuyệt nếu bạn có thể giúp tất cả các developer thực sự dễ dàng tuân theo hầu hết các nguyên tắc mà bạn có với rất ít công việc phải không? Điều gì sẽ xảy ra nếu, ngay từ đầu, các developer đã có hầu hết các mã nguồn để deploy các thuộc tính cốt lõi mà mỗi dịch vụ cần?
Dropwizard và Karyon là hai micro container mã nguồn mở, dựa trên JVM. Chúng hoạt động theo những cách tương tự, tập hợp một bộ các thư viện lại với nhau để cung cấp các tính năng như kiểm tra tình trạng, phục vụ HTTP hoặc hiển thị số liệu. Vì vậy, ngay từ đầu, bạn đã có một dịch vụ hoàn chỉnh với một servlet container có thể được nhúng và khởi chạy từ dòng lệnh. Đây là một cách tuyệt vời để bắt đầu, nhưng tại sao lại dừng lại ở đó? Trong khi bạn đang sử dụng nó, tại sao không lấy một cái gì đó như Dropwizard hoặc Karyon và thêm nhiều tính năng hơn để nó trở nên phù hợp với ngữ cảnh của bạn?
Ví dụ, bạn có thể muốn bắt buộc sử dụng bộ ngắt mạch. Trong trường hợp đó, bạn có thể tích hợp một thư viện như Hystrix. Hoặc bạn có thể có một thực tế rằng tất cả các chỉ số của bạn cần phải được gửi đến một máy chủ Graphite trung tâm, vì vậy có thể kéo thư viện mã nguồn mở như Dropwizard’s Metrics và định cấu hình nó để thời gian phản hồi và tỷ lệ lỗi được đẩy tự động đến một vị trí đã biết.
Bằng cách điều chỉnh một khuôn mẫu dịch vụ như vậy cho tập hợp các phương pháp phát triển của riêng bạn, bạn đảm bảo rằng các nhóm có thể tiến hành nhanh hơn và các developer cũng phải cố gắng làm cho dịch vụ của họ hoạt động dù trong điều kiện không tốt.
Tất nhiên, nếu bạn chấp nhận nhiều stack công nghệ khác nhau, bạn sẽ cần một khuôn mẫu dịch vụ phù hợp cho từng loại. Tuy nhiên, đây có thể là một cách bạn hạn chế một cách tinh vi các lựa chọn ngôn ngữ trong nhóm của mình. Nếu mẫu dịch vụ nội bộ chỉ hỗ trợ Java, thì mọi người có thể không khuyến khích chọn các stack thay thế nếu họ phải tự làm nhiều việc hơn. Netflix, chẳng hạn, đặc biệt quan tâm đến các khía cạnh như khả năng chịu lỗi, để đảm bảo rằng sự cố ngừng hoạt động của một bộ phận trong hệ thống của họ không thể khiến mọi thứ không hoạt động theo. Để xử lý điều này, một lượng lớn công việc đã được thực hiện để đảm bảo rằng có các thư viện trên JVM để cung cấp cho các nhóm các công cụ họ cần để giữ cho các dịch vụ của họ hoạt động tốt. Bất kỳ ai giới thiệu một stack công nghệ mới có nghĩa là phải tái tạo tất cả nỗ lực này. Mối quan tâm chính đối với Netflix không phải là về nỗ lực trùng lặp mà thiên về thực tế là rất dễ mắc phải sai lầm này. Rủi ro mà dịch vụ không thể chịu lỗi khi có nhưng phần được làm mới khi deploy là cao nếu nó có thể ảnh hưởng nhiều hơn đến hệ thống. Netflix giảm thiểu điều này bằng cách sử dụng các dịch vụ sidecar, giao tiếp cục bộ với một JVM đang sử dụng các thư viện thích hợp.
Bạn phải cẩn thận rằng việc tạo khuôn mẫu dịch vụ không trở thành công việc của một nhóm công cụ hoặc một nhóm kiến trúc sư tập trung, những người chỉ định cách mọi thứ nên được thực hiện, mặc dù thông qua mã nguồn. Việc xác định các phương pháp bạn sử dụng phải là một hoạt động tập thể, vì vậy, lý tưởng nhất là (các) nhóm của bạn nên chịu trách nhiệm chung về việc cập nhật khuôn mẫu này (phương pháp tiếp cận nguồn mở nội bộ hoạt động tốt ở đây).
Tôi cũng đã thấy tinh thần và năng suất của nhiều nhóm bị suy giảm nghiêm trọng khi có một framework bị áp đặt sử dụng. Trong nỗ lực cải thiện khả năng tái sử dụng mã nguồn, ngày càng nhiều công việc được đặt vào một framework tập trung cho đến khi nó trở thành một con quái vật khổng lồ. Nếu bạn quyết định sử dụng một khuôn mẫu dịch vụ phù hợp, hãy suy nghĩ thật kỹ về công việc của nó. Lý tưởng nhất, việc sử dụng nó nên hoàn toàn là tùy chọn, nhưng nếu bạn muốn áp dụng nó một cách mạnh mẽ hơn, bạn cần hiểu rằng tính dễ sử dụng cho các developer sẽ là động lực chính.
Cũng cần lưu ý về những nguy cơ khi mã nguồn được chia sẻ lại. Với mong muốn tạo ra mã có thể sử dụng lại, chúng ta có thể đã tạo ra nguồn gốc của sự bó buộc (coupling) giữa các dịch vụ. Ít nhất một tổ chức mà tôi đã nói chuyện với họ, họ lo lắng về điều này đến nỗi nó thực sự đã sao chép mã nguồn khuôn mẫu dịch vụ của mình theo cách thủ công vào mỗi dịch vụ. Điều này có nghĩa là việc nâng cấp lên khuôn mẫu dịch vụ cốt lõi sẽ mất nhiều thời gian hơn để được áp dụng trên toàn hệ thống, nhưng điều này ít liên quan đến nó hơn là nguy cơ của sự bó buộc. Các nhóm khác mà tôi đã nói chuyện chỉ đơn giản coi mẫu dịch vụ là một dependency kiểu nhị phân (đã được compile) được chia sẻ, mặc dù họ phải rất chăm chỉ trong việc không để bị DRY (don’t repeat yourself – đừng lặp lại chính mình) dẫn đến một hệ thống kết hợp quá chặt chẽ với nhau! Đây là một chủ đề mang nhiều sắc thái, vì vậy chúng ta sẽ khám phá chi tiết hơn trong Chương 4.
[note1]: Đây là một nguyên tắc trong việc phát triển phần mềm, nói đến việc không để mã nguồn bị lặp lại. Trong cuốn The Pragmatic Programer thì Dry được định nghĩa như sau: “Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.”
Nợ kỹ thuật – Technical Debt
Chúng ta thường bị đặt vào những tình huống mà chúng ta không thể theo kịp về tầm nhìn kỹ thuật của chúng ta. Thông thường, chúng ta cần phải lựa chọn cắt một vài góc để có được một số tính năng cấp thiết. Đây chỉ là một sự đánh đổi nữa mà chúng ta sẽ thấy mình phải thực hiện. Tầm nhìn kỹ thuật của chúng ta tồn tại là có lý do. Nếu chúng ta đi chệch khỏi lý do này, nó có thể mang lại lợi ích ngắn hạn nhưng phải trả giá dài hạn. Một khái niệm giúp chúng ta hiểu sự đánh đổi này là nợ kỹ thuật (technical debt). Khi chúng ta tích lũy món nợ này, cũng giống như nợ trong thế giới thực, nó có chi phí liên tục và là thứ chúng ta muốn trả bớt.
Đôi khi nợ kỹ thuật không chỉ là thứ mà chúng ta gây ra bằng cách đi tắt. Điều gì xảy ra nếu tầm nhìn của chúng ta đối với hệ thống thay đổi, nhưng không phải tất cả hệ thống của chúng ta đều phù hợp? Trong tình huống này, chúng tôi đã tạo ra các nguồn nợ kỹ thuật mới.
Công việc của kiến trúc sư hệ thống là nhìn vào bức tranh toàn cảnh hơn và hiểu được sự cân bằng này. Có một số quan điểm về mức độ nợ và những chỗ ảnh hưởng, là điều quan trọng. Tùy thuộc vào tổ chức của bạn, bạn có thể cung cấp một hướng dẫn theo kiểu nhẹ nhàng, và để các nhóm tự quyết định cách theo dõi và thanh toán khoản nợ. Đối với các tổ chức khác, bạn có thể cần phải làm việc có nguyên tắc và cấu trúc hơn, có thể là duy trì việc log những khoản nợ và xem xét thường xuyên.
Xử lý ngoại lệ
Vì vậy, các nguyên tắc và thực tiễn của chúng ta hướng dẫn cách xây dựng hệ thống của chúng ta. Nhưng điều gì xảy ra khi hệ thống của chúng ta đi chệch hướng này? Đôi khi chúng ta đưa ra quyết định chỉ là một ngoại lệ của quy tắc. Trong những trường hợp này, bạn nên ghi lại quyết định như vậy vào log ở đâu đó để tham khảo trong tương lai. Nếu tìm thấy đủ các trường hợp ngoại lệ, chúng ta cũng có thể thay đổi nguyên tắc hoặc thông lệ để phản ánh một cách hiểu mới về thế giới. Ví dụ, chúng tôi có thể có một thông lệ nói rằng chúng ta sẽ luôn sử dụng MySQL để lưu trữ dữ liệu. Nhưng sau đó chúng tôi thấy những lý do thuyết phục để sử dụng Cassandra để lưu trữ có khả năng mở rộng cao, tại thời điểm đó, chúng tôi thay đổi cách nói của mình để nói, “Sử dụng MySQL cho hầu hết các yêu cầu lưu trữ, trừ khi bạn mong đợi sự tăng trưởng lớn về khối lượng dữ liệu, trong trường hợp đó hãy sử dụng Cassandra.”
Tuy nhiên, tôi cần phải nhắc lại rằng mọi tổ chức đều khác nhau. Tôi đã làm việc với một số công ty nơi các nhóm phát triển có mức độ tin cậy và quyền tự chủ cao và ở đó các nguyên tắc rất gọn nhẹ (và nhu cầu xử lý ngoại lệ công khai sẽ giảm đáng kể nếu không bị loại bỏ). Trong các khu tổ chức có cấu trúc hơn, trong đó các developer có ít tự do hơn, việc theo dõi các ngoại lệ có thể rất quan trọng để đảm bảo rằng các quy tắc được đưa ra phản ánh đúng những thách thức mà mọi người đang phải đối mặt. Với tất cả những gì đã nói, tôi là một fan hâm mộ của microservices như một cách tối ưu hóa quyền tự chủ của các nhóm, mang lại cho họ nhiều quyền tự do nhất có thể để giải quyết vấn đề trong tầm tay. Nếu bạn đang làm việc trong một tổ chức đặt ra nhiều hạn chế về cách các developer có thể thực hiện công việc của họ, thì microservices có thể không dành cho bạn.
Quản trị và Lãnh đạo từ Trung tâm
Một phần của những gì kiến trúc sư hệ thống cần xử lý là quản trị. Quản trị mà tôi muốn đề cập đến là gì? Hóa ra Kiểm soát mục tiêu đối với Công nghệ Thông tin và những thứ Liên quan (Control Objectives for Information and Related Technology – COBIT) có một định nghĩa khá hay:
Quản trị đảm bảo rằng các mục tiêu của doanh nghiệp đạt được bằng cách đánh giá các nhu cầu, điều kiện và lựa chọn của các bên liên quan; thiết lập phương hướng thông qua ưu tiên và ra quyết định; và giám sát việc thực hiện, tuân thủ và tiến độ so với chỉ đạo và mục tiêu đã thống nhất.
— COBIT 5
[note2]: đây một framework được đề xuất bởi ISACA (Hiệp hội Kiểm tra và Kiểm soát Hệ thống Thông tin), nhằm giúp các tổ chức đang tìm cách phát triển, triển khai, giám sát và cải thiện quản trị CNTT và quản lý thông tin.
Quản trị có thể áp dụng cho nhiều thứ trong diễn đàn CNTT. Chúng tôi muốn tập trung vào khía cạnh quản trị kỹ thuật, điều mà tôi cảm thấy là công việc của kiến trúc sư. Nếu một trong những công việc của kiến trúc sư là đảm bảo tầm nhìn kỹ thuật, thì quản trị là đảm bảo những gì chúng tôi đang xây dựng phù hợp với tầm nhìn này và phát triển tầm nhìn nếu cần.
Kiến trúc sư chịu trách nhiệm về rất nhiều thứ. Họ cần đảm bảo có một bộ nguyên tắc có thể hướng dẫn sự phát triển và những nguyên tắc này phù hợp với chiến lược của tổ chức. Họ cũng cần đảm bảo rằng những nguyên tắc này không yêu cầu các phương pháp làm việc khiến các developer phải khổ sở vì nó. Họ cần cập nhật công nghệ mới và biết khi nào cần đánh đổi đúng. Đây là một trách nhiệm lớn khủng khiếp. Tất cả những điều đó, và họ cũng cần kéo mọi người theo — nghĩa là, để đảm bảo rằng các đồng nghiệp mà họ đang làm việc hiểu được các quyết định đang được đưa ra và được đưa vào thực tế để thực hiện chúng. Ồ, và như chúng tôi đã đề cập: họ cần dành một chút thời gian với các nhóm để hiểu tác động của các quyết định của họ và thậm chí có thể viết mã nguồn nữa.
Đó là một yêu cầu cao? Chắc chắn rồi. Nhưng tôi chắc chắn với quan điểm rằng họ không nên làm điều này một mình. Một nhóm quản trị hoạt động tốt có thể làm việc cùng nhau để chia sẻ công việc và định hình tầm nhìn.
Thông thường, quản trị là hoạt động của nhóm. Đó có thể là một cuộc trò chuyện thân mật với một nhóm đủ nhỏ hoặc một cuộc họp thường xuyên có cấu trúc hơn với tư cách thành viên nhóm chính thức cho phạm vi lớn hơn. Đây là lúc tôi nghĩ các nguyên tắc mà chúng ta đã đề cập trước đó nên được thảo luận và thay đổi theo yêu cầu. Nhóm này cần được dẫn dắt bởi một tay công nghệ và chủ yếu bao gồm những người đang thực hiện công việc được quản lý. Nhóm này cũng phải chịu trách nhiệm theo dõi và quản lý các rủi ro kỹ thuật.
Một mô hình mà tôi rất thích là có kiến trúc sư chủ trì nhóm, nhưng có phần lớn nhóm được thu hút từ các tay công nghệ của mỗi nhóm deliver sản phẩm — tối thiểu là những nhóm trưởng. Kiến trúc sư chịu trách nhiệm đảm bảo nhóm hoạt động, nhưng toàn bộ nhóm chịu trách nhiệm quản trị. Điều này chia sẻ công việc của kiến trúc sư và đảm bảo rằng có mức độ tham gia cao hơn vào các quyết định từ các nhóm. Nó cũng đảm bảo rằng thông tin luân chuyển tự do từ các nhóm vào toàn bộ tập thể và kết quả là việc đưa ra quyết định trở nên hợp lý và đầy đủ thông tin hơn nhiều.
Đôi khi, nhóm có thể đưa ra quyết định mà kiến trúc sư không đồng ý. Lúc này, kiến trúc sư phải làm gì? Tôi đã gặp tình trạng này trước đây, và có thể nói với bạn đây là một trong những tình huống khó khăn nhất phải đối mặt. Thông thường, tôi thực hiện cách tiếp cận mà tôi nên đi với quyết định của nhóm. Tôi cho rằng tôi đã cố gắng hết sức để thuyết phục mọi người, nhưng cuối cùng thì tôi thấy không đủ thuyết phục. Nhóm thường khôn ngoan hơn nhiều so với từng cá nhân, và tôi đã nhiều lần bị chứng minh là sai! Và hãy tưởng tượng việc một nhóm được cho không gian để đưa ra quyết định, và cuối cùng bị bỏ qua. Nhưng đôi khi tôi đã bác bỏ ý kiến của cả nhóm. Nhưng tại sao, và khi nào? Làm thế nào để bạn chọn cách làm?
Hãy nghĩ đến việc dạy trẻ đi xe đạp. Bạn không thể đi hộ cho họ. Bạn thấy chúng chao đảo, nhưng nếu bạn tham gia vào mỗi lần như thể chúng có thể ngã ra, thì chúng sẽ không bao giờ học được và trong mọi trường hợp, chúng sẽ ngã ra ít hơn bạn nghĩ! Nhưng nếu bạn thấy họ chuẩn bị lao vào dòng xe cộ hoặc vào một cái ao gần đó, thì bạn phải can thiệp. Tương tự như vậy, là một kiến trúc sư, bạn cần phải nắm chắc khi nào, theo nghĩa bóng, nhóm của bạn đang lái vào một cái ao. Bạn cũng cần lưu ý rằng ngay cả khi bạn biết mình đúng và bác bỏ ý kiến của nhóm, điều này có thể làm suy yếu vị trí của bạn và cũng khiến nhóm cảm thấy rằng họ không hề có tiếng nói. Đôi khi điều đúng đắn là đi cùng với một quyết định mà bạn không đồng ý. Biết khi nào nên làm điều này và khi nào không nên làm điều này là khó khăn, nhưng đôi khi đó lại là một điều quan trọng.
Xây dựng đội ngũ
Trở thành người chính chịu trách nhiệm về tầm nhìn kỹ thuật của hệ thống của bạn và đảm bảo rằng bạn đang thực hiện tầm nhìn này không chỉ là việc đưa ra quyết định về công nghệ. Chính những người bạn làm việc cùng sẽ thực hiện công việc. Phần lớn vai trò của người lãnh đạo kỹ thuật là giúp phát triển họ — giúp họ tự hiểu tầm nhìn — và cũng đảm bảo rằng họ cũng có thể là những người tham gia tích cực trong việc định hình và thực hiện tầm nhìn.
Giúp những người xung quanh bạn phát triển sự nghiệp có thể có nhiều hình thức, hầu hết đều nằm ngoài phạm vi của cuốn sách này. Tuy nhiên, có một khía cạnh mà kiến trúc microservice đặc biệt có liên quan. Với các hệ thống lớn hơn, monolithic, có ít cơ hội hơn để mọi người nâng cấp và sở hữu thứ gì đó. Mặt khác, với các microservice, chúng tôi có nhiều mã nguồn cơ sở tự trị và sẽ có các vòng đời độc lập của riêng chúng. Giúp mọi người thăng tiến bằng cách để họ làm chủ các dịch vụ riêng lẻ trước khi nhận thêm trách nhiệm có thể là một cách tuyệt vời để giúp họ đạt được mục tiêu nghề nghiệp của riêng mình, đồng thời giảm bớt gánh nặng cho người phụ trách!
Tôi rất tin tưởng rằng phần mềm tuyệt vời đến từ những con người tuyệt vời. Nếu bạn chỉ lo lắng về khía cạnh công nghệ của phương trình, bạn đang thiếu một nửa bức tranh rồi đó.
Tóm tắt
Để tóm tắt chương này, đây là những gì tôi thấy là trách nhiệm cốt lõi của kiến trúc sư khi phát triển:
Tầm nhìn
Đảm bảo có một tầm nhìn kỹ thuật được truyền đạt rõ ràng cho hệ thống sẽ giúp hệ thống của bạn đáp ứng các yêu cầu của khách hàng và tổ chức của bạn
Đồng cảm
Hiểu tác động của các quyết định của bạn đối với khách hàng và đồng nghiệp của bạn
Sự hợp tác
Tương tác với càng nhiều đồng nghiệp và đồng nghiệp của bạn càng tốt để giúp xác định, tinh chỉnh và thực hiện tầm nhìn
Khả năng thích ứng
Đảm bảo rằng tầm nhìn kỹ thuật thay đổi khi khách hàng hoặc tổ chức của bạn yêu cầu
Quyền tự trị
Tìm sự cân bằng phù hợp giữa tiêu chuẩn hóa và tạo quyền tự chủ cho các nhóm của bạn
Quản trị
Đảm bảo rằng hệ thống đang được deploy phù hợp với tầm nhìn kỹ thuật
Kiến trúc sư của hệ thống khi tiến hóa là người hiểu rằng để đạt được kỳ tích này là một sự cân bằng liên tục. Các lực đẩy luôn thúc đẩy bạn theo cách này hay cách khác, và việc đứng ở đâu để đẩy lùi hoặc đi đâu với dòng chảy thường là điều thường đi liền với kinh nghiệm. Nhưng phản ứng tồi tệ nhất đối với tất cả những lực đẩy chúng ta đến sự thay đổi là trở nên cứng nhắc hoặc cố định hơn trong suy nghĩ của chúng ta.
Mặc dù phần lớn lời khuyên trong chương này có thể áp dụng cho bất kỳ kiến trúc sư của hệ thống nào, nhưng các microservice cho chúng ta nhiều quyết định hơn để đưa ra. Vì vậy, có thể cân bằng tốt hơn tất cả những sự đánh đổi này là điều cần thiết.
Trong chương tiếp theo, chúng ta sẽ đưa ra một số nhận thức mới về vai trò của kiến trúc sư đối với chúng ta khi chúng ta bắt đầu suy nghĩ về cách tìm ra ranh giới phù hợp cho microservices.
Như mình đã nói ở phần đầu hard skill của developer và software architect khá rõ ràng về mặt tiếp cận, có nhiều nguồn tài liệu để đọc mình có thể list ra ở đây
Domain-Driven Design: Tackling Complexity in the Heart of Software
Design It! – From Programmer to Software Architect
Software Architecture in Practice
Design Patterns – Elements of Reusable Object-Oriented Software
Domain Driven Design
Systems Architecting: Creating & Building Complex Systems
Systemantics: How Systems Work and Especially How They Fail
Release It!: Design and Deploy Production-Ready Software (Pragmatic Programmers)
The Clean Architecture
Patterns of Enterprise Application Architecture
The Art of Scalability: Scalable Web Architecture, Processes, and Organizations for the Modern Enterprise
Martin Fowler – Patterns of Enterprise Application Architecture (2002)
Trong đó quyển của Martin Fowler khá đầy đủ và chi tiết.
Vậy chúng ta cần đọc bao nhiêu sách, biết bao nhiêu loại pattern? Câu trả lời luôn là càng nhiều càng tốt
Chọn architecture cho dự án
Hay nói đúng hơn là khi bắt đầu dự án, chúng ta sẽ chọn kiến trúc nào: microservice, event-driven hay layered theo cách truyền thống.
Đấy là câu hỏi thường gặp và chả có gì sai khi hỏi như vậy cả vì ai cũng biết rằng phần mềm mà đưa ra thị trường kiểu gì chẳng được thiết kế theo kiến trúc nào đó.
Tuy nhiên câu trả lời lại thường khiến chúng ta bất ngờ
Big bang architecture
Nghe giống như Big Bang Model (một loại mô hình phát triển phần mềm), và đúng là như thế.
Khi dự án mới bắt đầu, chúng ta chưa hiểu gì về hệ thống cả, chỉ nên thiết kế và làm kiến trúc chỉ những gì cần thiết. Ví dụ chúng ta chỉ nên quyết định có thể cần integrate 2 hệ thống với nhau mà chưa cần quyết định xem sẽ làm việc đó như nào. Về cơ bản, chúng ta cần tiến hóa architecture theo dự án: những đều chúng ta biết thêm về hệ thống.
Rất lạ là yêu cầu phần mềm, công nghệ có thể sử dụng và đã sử dụng và business của công ty cũng như khách hàng cần thay đổi hằng ngày, nhưng chúng ta thường nghĩ rằng architecture cần phải cố định.
Thường thì mình sẽ chọn một architecture vừa đủ dùng trong một thời gian phù hợp với dự án và đội phát triển (tất nhiên không phải là phần mềm kiểu enterprise rồi)
Và gần như kiến trúc này sẽ được cải tiến cùng với CI/CD trong phần trước đó, nếu kiến trúc đó có vấn đề khi tích hợp với Pipeline thì có nghĩa rằng kiến trúc hoặc Pipeline hoặc cả 2 đều có vấn đề và chúng cần được cải tiến.
Một dự án cần phát triển một ứng dụng như Tiki chẳng hạn, nhưng Architect lại được bắt đầu với multi tier truyền thống và bạn sẽ có một monolithic web application, ai cũng sẽ thấy rằng, khi bạn cần sửa dù chỉ một phần nhỏ trong header của request, bạn sẽ cần deploy lại cả hệ thống, điều đó không sai nhưng rõ ràng có vấn đề. Điều tất yếu xảy ra kiến trúc sẽ được cải tiến.
Gần như hiện tại trong quá trình làm dự án, chúng ta có thể thấy rằng nếu như hệ thống có hình dáng giống với một hệ thống nào đó trong quá khứ thì chúng ta sẽ gần như kế thừa và giữ nguyên, trong khi đó các hệ thống chưa xuất hiện bao giờ thì sẽ được quyết định bởi trực giác của Architect trong một quá trình khám phá có kiểm soát.
Các quyết định này có thể đúng, sai, tăng tiến độ hoặc thậm chí phá hỏng mọi thứ và chúng ta phải rework.
Khi đó chúng ta có một thứ gọi là Accidental Architecture, tuy nhiên, điều này chưa hẳn đã xấu vì gần như chúng ta không thể tránh khỏi trong quá trình làm dự án và xây dựng hệ thống. Chỉ khi bắt đầu biến những kiến trúc ngẫu nhiên này thành những kiến trúc có chủ đích, chúng ta mới nâng cao hiểu biết của mình về kiến trúc phần mềm.
Final words
Có lẽ serires này sẽ dừng ở đây, mình sẽ cố gắng sắp xếp lại một số bài viết liên quan đến chủ đề này trong nhưng bài viết khác.
Trong phần trước, mình đã nói về Soft Skill — Decision, trong phần này sẽ đề cập tới một loại khác mà sẽ gây nhiều tranh cãi: Continuous Delivery.
Continuous Delivery, hãy nói về mặt khả năng, chính xác hơn là điểm khác biệt với Manual Delivery ở 3 yếu tố: an toàn, nhanh chóng và bền vững (hoặc dùng từ Việt một chút là ổn định).
Trong bài viết này sẽ không đánh đồng Continuous Delivery với các kỹ thuật automate, mà mọi người hay khá nhầm lẫn với việc Automation Delivery (một trong những kỹ thuật của CD). Dù sao thì, mình chỉ muốn xem xét về mặt văn hóa trong dự án phần mềm.
Tại sao cần Continuous Delivery?
Thời gian — tất nhiên rồi, ai cũng muốn đưa những tính năng mới nhất cho người dùng một cách nhanh nhất, đưa hệ thống trở lại khi downtime nhanh nhất (Mean Time To Repair) đồng nghĩa với việc business được duy trì, tiền kiếm được không bị ngừng trệ.
Hãy xem xét 2 loại thời gian:
Lead time: lượng thời gian từ khi bắt đầu và kết thúc của một công việc trong quy trình
Cycle time: khoảng thời gian mà giữa 2 công việc liên tiếp được kết thúc, hay còn có thể hiểu là Deployment Frequency cho dễ hiểu
Continuous Delivery Pipeline sẽ được hình dung như này
Còn đây là Lead time
Đây là Cycle time nè
Vậy thời gian bao nhiêu là đủ?
Credit: Forsgren PhD, Nicole. Accelerate: The Science of Lean Software and DevOps: Building and Scaling High Performing Technology Organizations (Kindle Location 564). IT Revolution Press. Kindle Edition.
Nhìn chung thì khi chúng ta cố gắng tăng tần suất deploy chúng ta sẽ có thể giảm MTTR, có nghĩa chúng ta sẽ không tạo ra một sản phẩm có tính bền vững cao (robustness — như cách tiếp cận truyền thống), khi đó khả năng tự phục hồi sau lỗi (resilience) của sản phẩm được tăng lên (một cách tự nhiên), cùng với đó là giảm khả năng xảy ra lỗi vì gần như khi tăng tần suất deploy đến hằng ngày hoặc nhiều lần một ngày sản phẩm sẽ gần như lúc nào cũng sẵn sàng để đưa lên môi trường production (production ready).
Như vậy chúng ta sẽ có một mô hình:
Continuous Integration:
Integration sớm và thường xuyên
Mọi người cần đồng bộ trunk (ý nói đến phiên bản được deploy) hằng ngày
Continuous Deployment:
Điều này có để dễ dàng đạt được, khi source code ở trunk khá ổn định và Deploy chỉ là stage cuối cùng của Continuous Integration, như vậy việc phát hiện các lỗi và fix chúng cũng nhanh hơn, kể cả khi bạn có những thành viên không quá giỏi trong tay. Với các công cụ khác nhau như Sonarquebe hay chính Unit Test sẽ hỗ trợ developer tạo nên những sản phẩm tốt hơn.
Continuous Delivery:
Sau 2 vấn đề bên trên source code ổn định, deploy thường xuyên, bản vá được xử lý nhanh chóng ít ảnh hưởng, cuối chùng chúng ta có một phần mềm luôn ở trạng thái sẵn sàng.
Nhưng
Có thể các bạn sẽ nghĩ hay có câu hỏi như này: Lỗi vẫn xảy ra thì sao?
Ồ thì tất nhiên lỗi vẫn xảy ra, nhưng chúng ta sẽ có lỗi ở những thời điểm sớm hơn.
Hầu hết mọi người sẽ gặp các vấn đề thường xảy ra khi chúng ta tích hợp (các thành phần trong phần mềm với nhau), điều này khá khó chịu, và mọi người sẽ có xu hướng trì hoãn.
Tuy nhiên hãy nghĩ đến trường hợp này: trong ví dụ ở phần trước giả sử client cứ làm theo requirement hoặc UX/UI được cung cấp, backend thiết kế hoàn toàn khác tuy nhiên vẫn đầy đủ thông tin, client sẽ phải call nhiều API cho màn hình thay vì 1 API như trong suy nghĩ, điều đó dẫn đến việc một trong 2 cần thay đổi, việc này tốn khá nhiều công và tiềm ẩn rủi ro. Vậy nên khi tích hợp sớm chúng ta sẽ làm việc trơn tru trong quãng thời gian còn lại và sẵn sàng release thay vì dành vài tuần hoặc vài ngày cuối để thay đổi toàn bộ interface.
Bring the pain forward
Hay đầy đủ là if it hurts, do it more often, and bring the pain forward.
Quay lại vấn đề giữa mobile và backend, giả sử sẽ vẫn có những khác biệt về suy nghĩ xây dựng interface giữa 2 bên, tuy nhiên chúng ta đã làm nó sớm, và chia nhỏ nó ra và bằng cách backend thường xuyên release phiên bản API mới (chưa nói đến việc backward compatibility nhé), mobile sẽ tích hợp tốt hơn, nhanh hơn vì những thay đổi khá nhỏ và dễ dàng đáp ứng. Trong khi đó mobile team cũng build ra app thường xuyên hơn, tester có thể verify nhanh hơn, sản phẩm được ổn định hơn, ít tiềm ẩn lỗi hơn.
Điều này khá giống trong lý thuyết phát triển phần mềm, chi phí sửa lỗi càng nhiều cho các lỗi ở giai đoạn muộn của dự án.
Continuous Delivery bằng cách nào
Continuous Delivery cho chúng ta một cách tiếp cận khác về việc quản lý dự án, chúng ta có thể định nghĩa lại done không phải là code xong mà là được delivery thành công.
Toàn bộ quá trình nên sử dụng đối đa các nền tảng tự động như Unit-test, Automation Test, điều đó làm tăng tốc độ feedback trên các bản vá của source code cũng như infrastructure.
Điều này càng quan trọng hơn khi chi phí End-to-end testing càng ngày càng lớn, chúng nên được thay thế bằng các danh mục testing như dưới đây (multi-faceted testing portfolio), điều này càng quan trọng hơn trong thế giới hiện nay khi Devops là xu hướng, và các quyết định về release càng ngày càng gắn liên với tình huống về mặt nghiệp vụ thay vì sự quyết định của một đội quản lý về vận hành.
Practice
Quantity
Frequency
Duration
Environment
Unit Testing
100 to 1000+
Per build
< 30s total
Local and Build
Acceptance Testing
10 to 100+
Per build
< 10m total
Local and Build
Exploratory Testing
10 to 100+
Per build
Timebox
Local and 3rd Party
Contract Testing
~20
Per 3rd party deploy
< 1m
3rd Party
Smoke Testing
~5
Per deploy
< 5m
All
Monitoring
10 to 100+
Always
< 10s
All
Anomaly detection
10 to 100+
< 1m
< 10s
All
Adaptive architecture
N/A
Always
N/A
All
Như vậy có thể thấy là việc xử lý Continuous Delivery gần như không mang tính kỹ thuật mà mang tính định hướng về văn hóa làm việc cho dự án nhiều hơn, chính vì thế tại sao mình lại đặt nó là Soft Skill.
Như ở phần trước mình đã nói về các Soft Skills mà mình cho là yếu tố khác biệt giữa Developer và Software Architecture.
Decision
Về cơ bản thì với vai trò là Software Architecture bạn sẽ cần quyết định nhiều thứ.
Đầu tiên hãy kể đến chính Kiến trúc, thứ mà bạn đang xây dựng.
Nếu bạn có vài trò là người quyết định kiến trúc, bạn gần như sẽ là người đảm bảo duy nhất cho sự toàn vẹn của hệ thống, về mặt khái niệm thôi nhé. Tức là bạn sẽ là người đầu tiên hình dung ra hình dáng của hệ thống và thông thường những quyết định kiểu này sẽ được đặt ra trong những phase đầu tiên của phần mềm.
Nhiều người sẽ thắc mắc nếu quyết định đó là sai, thực tế thì những quyết định bởi những người có kinh nghiệm sẽ thường phù hợp với tình hình ở thời điểm đó.
Một câu hỏi khác: tại sao chỉ một người đảm bảo duy nhất cho sự toàn vẹn? Thông thường ở một mức độ nào đó, trong một đội dự án, Architect sẽ có sự hiểu biết tốt nhất về hệ thống cũng như các hard skill khác, và họ sẽ có một cách hình dung khá chung với những người có trình độ tương đương. Tuy nhiên việc này sẽ dẫn tới việc nghẽn cổ chai ở Architect, vậy nên việc này sẽ dẫn tới một khái niệm khác là Architectural Knowledge Management hay Software Architecture Knowledge Management (điều này giải thích tại sao các team thường có wiki hay những bản tài liệu được chia sẻ về kiến thức) Read more
Vậy thực sự có thể nói họ sẽ làm những việc như này
Thiết kế kiến trúc
Định ra các nguyên tắc để quyết định về mặt công nghệ.
Ví dụ:
Bạn sẽ cần đưa ra quyết định về việc sử dụng API Gateway để làm một endpoint duy nhất cho cả mobile và web hay sẽ sử dụng Backend for Frontend trong mô hình microservice.
Ồ thì dẫn đến một điều bạn cứ trả lời 2 câu hỏi dưới đây bạn sẽ biết là bạn có đang đưa ra các quyết định về mặt kiến trúc hay không:
Mức độ ảnh hưởng có ở toàn hệ thống hay không? Độ khó để implement quyết định đó?
Quyết định đó có giúp team quyết định về mặt công nghệ hoặc chỉ định cho họ.
Một công việc hằng ngày bạn có thể thấy mấy tên Architect hay làm đó là ngồi tìm hiểu thực sự bạn đang làm gì trong dự án, bạn có hiểu vấn đề chung của dự án hay không, tìm các vấn đề quan trọng và giải quyết trước khi mọi thứ trở nên tồi tệ (chỗ này định ghi là một bãi rác).
Trong một dự án IoT gần đây mình có tham gia, Architect của dự án đã quyết định cho team backend dừng công việc để chỉnh trang lại kiến thực chung.
Hoặc đơn giản hơn, Architect có thể ngồi với developer để tìm ra một số đoạn code gây lock hệ thống, buổi chiều có thể sharing với dự án về những thứ khá phi kỹ thuật như việc những đoạn code của họ sẽ gây ảnh hưởng đến chi phí AWS.
Bạn có thể thấy rằng việc bạn không thể hình dung dự án đang làm ra sản phẩm như nào rất nguy hiểm, giả như rằng bạn khăng khăng bạn sẽ phải code lại toàn bộ luồng OAuth2.0 cho toàn bộ các microservice để dùng chung một endpoint, nhưng thực tế là hoàn toàn bạn có thể để chúng riêng rẽ, điều quan tâm duy nhất là dùng chung Identity Pool, việc làm đó có thể khiến những team khác trong dự án bị trễ lịch đến hàng tuần.
Tính đúng đắn của quyết định — Justifying decisions
Rõ ràng quyết định đã được đưa ra, điều đó là tất yếu trong hoàn cảnh đồng hồ của dự án bắt đầu tính giờ.
Tuy nhiên thì không phải quyết định nào cũng được chấp nhận (chỉ là được chấp nhận nhé, chưa bàn đến tính đúng sai)
Hãy quay trở lại ví dụ bên trên về API Gateway và Backend for Frontend, đây thực chất là kết quả sau khi Architect đã xem xét rõ ràng (đúng hoặc sai), còn quá trình thì sao.
Để bất đầu hãy xem xét kịch bản tại sao lại có sự lựa chọn:
Giả sử bạn đang xây dựng một cửa hàng trực tuyến sử dụng Microservice và bạn đang triển khai trang product detail. Bạn cần phát triển nhiều phiên bản của giao diện người dùng chi tiết sản phẩm: cho cả web browser dưới dạng HTML5 hoặc Rest API để phục vụ cho mobile.
Giao diện này cần có nhiều thông tin: thông tin về sản phẩm, số lượng, lịch sử mua, các tùy chọn mua, các sản phẩm thường được mua cùng (một dang recommendation), review, rating của người bạn…
Và chúng ta có các service sau:
Product Info Service
Pricing Service
Order service
Inventory service
Review service
Recommendation service
Như vậy trang product detail cần tất cả các service trên
Bài toán được đặt ra là:
Làm cách nào để client của một ứng dụng Microservices truy cập vào các dịch vụ riêng lẻ?
Quyết định
Api Gateway
Sử dụng API Gateway là entrypoint duy nhất cho tất cả các client. API Gateway xử lý các yêu cầu theo một trong hai cách.
Một số yêu cầu chỉ đơn giản là proxied/routed đến service thích hợp.
Hoặc thay vì cùng cấp API kiểu one-size-fits-all, API Gateway cung cấp các API khác nhau cho từng client.
Backend For Frontend
BFF tạo ra các Gateway riêng rẽ cho từng loại Client.
Các điều kiện và ràng buộc (Conditions and Contraints)
Có nhiều yếu tô cần tính tới trong trường hợp này, nhưng mình sẽ tạm thời bỏ qua những vấn đề mang tính chủ quan của một dự án phần mềm: Con người (mặt bằng chung của team, broken comb, T-Shaped, …), process, stakeholder (rất nhiều khách hàng can thiệp vào architecture), mặc dù trên thực tế Architect thường nắm vai trò Technical Lead luôn nên sẽ cần cân nhắc. Hãy đi vào vấn đề kỹ thuật thôi.
Thường thì API do microservices cung cấp thường khác với những gì Client cần, về chi tiết nhé. Microservices thường cung cấp các API chi tiết, có nghĩa là client sẽ cần tương tác với nhiều service. Ví dụ, như được mô tả ở trên, client cần thông tin chi tiết về sản phẩm và cần tới nhiều dịch vụ.
Các Client khác nhau sẽ cần dữ liệu khác nhau. Ví dụ phần Recommendation cho phiên bản web sẽ nhiều hơn và có nhiều hành động hơn phiên bản API cho Mobile.
Đường truyền điện thoại với 3G, 4G sẽ khác với các trình duyệt ở trên máy tính được kế nối qua cáp quang cả về băng thông lẫn tính ổn định, thậm chí là chi phí. Có nghĩa là chúng ta có thể tăng số lượng request từ phía trình duyệt mà không ảnh hưởng quá nhiều đến trải nghiệm của người dùng lẫn chi phí băng thông, ngược lại với ứng dụng mobile.
Số lượng phiên bản của service và thông tin (host:port) thay đổi động
Việc thay đổi các phiên bản của service (upgrade, canary) không nên transparent với client
Một số dịch vụ được integration và sử dụng gRPC, webservice hoặc SOAP không thân thiện lắm với các client hiện nay.
Cân nhắc — Consideration
Với API Gatway chúng ta sẽ có một API thân thiện với client với protocol tiêu chuẩn. Việc triển khai Client sẽ đơn giản hơn vì logic của việc xử lý nhiều service sẽ nằm ở Gateway thay vì client.
Tuy nhiên dánh đổi bằng việc tăng độ phức tạp — API Gateway là một phần khác phải được phát triển, triển khai và quản lý, ngoài ra thời giản xử lý request cũng tăng lên vì thêm có bước xử lý network hop ở Gateway, tuy nhiên vân ngắn hơn thời gian xử lý cho BFF.
Quyết định về kiến trúc — Architecture decision
API Gateway
Biện luận
API Gatway với Spring có thể sử dụng Netflix Zuul, việc sử dụng Gateway sẽ đem đến trải nghiệm người dùng tốt hơn, vấn đề effort cho develop, deploy có thể giảm bằng cách đồng nhất cách thức xử lý cho các service sẽ đc xây dựng (dùng cùng một tech-stack)
Tài liệu hóa và trao đổi về quyết định về kiến trúc
Trao đổi về kiến trúc (giữa Architect và Developer)
Trong dự án phần mềm của bạn có các kênh trao đổi nào?
Email
Email (đơn thuần là email) thực tế không thích hợp để trao đổi nhất là về mặt kỹ thuật, có quá nhiều vấn đề trong quá trình xây dựng kiến trúc và thông tin trong email khá khó để tổng hợp và truyền đạt lại, hãy nghĩ đến việc bạn sẽ cho một thành viên mới dành cả 2 tuần lễ để đọc lại thread mail.
Hãy document quyết định về kiến trúc
Sử dụng wiki là một biện pháp ổn, nhanh, giao diện đẹp, dễ viết với Markdown và quan trọng nhất là tập trung. Một vấn đề nữa, hãy chỉ rõ hoặc quyết định luôn chỗ mà quyết định được lưu trữ (hơi nhiều từ quyết định, tho)
Với các vấn đề quan trọng và ảnh hưởng lớn, hãy đảm bảo tất cả mọi người cần biết được biết về nó. Bảng trắng luôn là một lựa chọn tốt để trao đổi, nếu trong thời buổi WFH có thể sử dụng các công cụ trong kênh giao tiếp.
Series này hưởng ứng Technopedia, nhưng các bài viết sẽ không phục vụ mục đích dự thi.
Đầu tiên mình định viết về Solution Architecture theo quyển Solutions Architect’s Handbook nhưng nghĩ lại thì Software Architecture sẽ hợp lý để bắt đầu hơn.
Disclaimer
Bài viết này không nhằm mục đích hướng dẫn hay đề ra một con đường trong sự nghiệp, chỉ là chia sẻ và những gì mình đã trải nghiệm thôi.
Developer đến Software Architecture
Mọi người sẽ thắc mắc tại sao lại là Developer chứ không phải Coder.
Về cơ bản thì Coder – Người code tức là họ chỉ biết hoặc cũng chỉ muốn code (Ở một phương diện nào đó), còn Developer – Người phát triển phần mềm, tức là họ xử lý tất cả các vấn đề liên quan đến quy trình phát triển phần mềm (Software Development Process) từ nói chuyển với stakeholders, thiết kế ứng dụng (ở một mức độ nào đó), deploy phần mềm, debug, sửa lỗi và cải tiến phần mềm.
Một cách đơn giản trong đa số trường hợp Coder tương đương với Junior Developer.
Ồ!!!
Vậy Developer sẽ trở thành Software Architecture như nào và điểm khác biệt là gì khi developer đã có thể thiết kế ứng dụng (như mình có nói ở trên).
Mình sẽ đưa ra ví dụ như này cho mọi người dễ hình dung:
Trong một dự án xây dựng hệ thống và phần mềm di động đi kèm một chiếc máy ảnh, người dùng có thể download một số sticker và chỉnh sửa lên bức ảnh của mình, Backend sẽ xây dựng một vài API để phục vụ.
Trong trường hợp này Developer đã có thể thiết kế được API, kiến trúc của Backend dựa trên ExpressJS, deploy chúng lên AWS, xử lý memcache để tốc độ phản hồi tốt hơn, người dùng nhận được sticker mới ngay khi chúng được upload.
Vậy có thể thấy rằng hầu hết công việc đã được Developer xử lý (ở đây tạm thời không phân chia Junior-Middle-Senior nhé) và dự án golive thành công. 5 tháng sau đội dự án bị đập tơi bời từ khách hàng vì bill từ AWS vài $10000 cho mỗi tháng, trong trường hợp này nếu là bạn Software Architecture sẽ xử lý như nào
(Thực ra có vài cách trong trường hợp này nhưng mình sẽ đưa ra một cách cho thấy sự tương phản nhất)
Thay vì đưa trực tiếp API cho Mobile như bạn Software Developer, API sẽ được xử lý như một file tĩnh (JSON-XML) từ S3 cùng với sự hỗ trợ của CDN, file này sẽ được cập nhật khoảng 30′ một lần nếu có sự thay đổi về danh sách sticker. Như vậy người dùng sẽ chờ tối đa khoảng 30′ để có thể nhận được sticker mới, nhưng thay vào đó bill từ AWS sẽ chỉ khoảng $500.
Vậy bạn Developer có thể biết cái gì tối ưu nhất cho hệ thống nhưng sẽ bị hạn chế khi xử lý các vấn đề tradeoffs-đánh đổi. Trong khi đó Architect sẽ cần biết cả 2.
Software architecture?
Đang nói về thiết kế ứng dụng chứ không phải job title nhé :P.
Mình sẽ gạch đầu dòng thôi vì chỗ nào cũng có định nghĩa:
Các tiêu chuẩn, khái niệm tôn chỉ (gì gì đó) của một hệ thống.
Cách tổ chức, các component quan trọng, cách tương tác, những thành phần nhỏ hơn và interface giữa chúng (không cụ thể nhé).
Về cơ bản thì Software architecture là những hiểu biết chung về thiết kế hệ thống trong dự án phần mềm, tất nhiên sẽ bao gồm cả việc hệ thống được chia nhỏ như nào, và interface giữa chúng là gì nữa, nhưng chỉ dừng lại ở những component và interface mà mọi người đều cần hiểu thôi nhé.
Ví dụ: chi tiết về việc iOS team lưu trữ password hay load thông tin từ database ở local device như nào sẽ không là một phần trong architecture của cả dự án nhưng việc chỉ ra những gì cần lưu trữ ở local như một lớp caching thì lại có.
So, this makes it hard to tell people how to describe their architecture. “Tell us what is important.” Architecture is about the important stuff. Whatever that is.
Vậy cần những gì để trở thành một Software Architecture?
Có rất nhiều bài viết nói về cái này, nhưng mình sẽ chỉ ra một số cái mình nghĩ là quan trọng nhất:
Hard skill hay technical skill gì gì đó:
Bao giờ cũng là các application architecture phổ biến, pattern, anti-pattern, cái này thì thực ra khá dễ để nhìn ra và dễ tiếp cận
Integration architecture, à thì không phải lúc nào cũng xây dựng một cái gì đó từ đầu (from scratch), nên integration luôn là sự lựa chọn tốt.
Enterprise architecture, nó liên quan đến tất cả mọi thứ của một công ty, cá thể kinh doanh, từ chiến lược kinh doanh, mô hình vận hành, hoạt động kinh doanh và cả những thứ liên quan đến IT và Infrastructure nữa, nên nếu có bị giới hạn về mặt hiểu biết, mình có thể sẽ không đi sâu về cái này.
Soft skill cái này quan trọng này, có thể mọi người sẽ nghĩ đến những thứ khá cao siêu như Leadership hay Organization nhưng mình sẽ đi từ những thứ nhỏ nhỏ trước:
Ra quyết định – Decisions, tất nhiên là bạn cần quyết định nhiều thứ, đặc biệt là về kiến trúc hoặc đơn giản hơn là trong team sẽ làm việc với nhau như nào (communication decision)
Continuous Delivery, nghe có vẻ là hard skill nhưng thực tế là soft skill. Về cơ bản phần mềm hay hệ thống sẽ vẫn là một thứ gì đấy trừu tượng (ở trên bản vẽ – mượn từ archiecture – kỹ sư trong ngành xây dựng), cho đến khi nó được deploy trên môi trường thực tế. Và khi phần mềm được deploy càng sớm và càng liên tục thì việc chứng mình architecture hợp lý càng nhanh và chính xác, mọi người cũng trưởng thành hơn về mặt thực hành.
Git và các dịch vụ Git như Github, Gitlab đang ngày càng trở nên phổ biến đối với các developer hiện này, hay có thể nói đó là phần không thể thiếu rồi.
Và đương nhiên, một developer có thể contribute đến nhiều project/repository, và mỗi project có thể dùng một định danh khác nhau. Ví dụ như dùng account email công ty với những project của công ty, hoặc email cá nhân với những project làm thêm, học thêm. Thậm chí có những người được khách hàng cấp account riêng để truy cập vào git riêng của họ.
Và nếu, developer không quản lý tốt git config cho từng project thì có thể sẽ bị nhầm lẫn email lúc commit lên. Đây là điều rất hay xảy ra nhưng tiếc là ít người để ý.
Nếu bạn làm trong FSoft, hoặc các công ty đề cao việc security đối với việc dùng email công ty cho các mục đích ngoài công việc, thì có thể nhận được những email warning dạng: bạn đang dùng email công ty trong project git này….hãy cho biết lý do tại sao, và một loạt các câu hỏi khác.
Tại sao IT lại biết được việc bạn push lên github, gần như ngay lập tức?
Github có API public các event xảy ra trên dịch vụ này gần như realtime tại: https://api.github.com/events. Và nếu tôi viết 1 con Bot scan liên tục API này và phân tích data thì tôi dễ dàng tìm được thông tin người thực hiện commit đấy lên như email hay user name (đương nhiên là email và user name config thôi)
Ví dụ như:
Github public events
Với sample phía trên, ta dễ dàng scan ra được user name và email của người vừa push lên. Và đương nhiên, con BOT của IT cũng sẽ tóm được ta nếu ta sử dụng email công ty để push lên.
Tuy nhiên, nhiều người sẽ thắc mắc tại sao với project này, tôi sử dụng email cá nhân để đăng ký, nhưng thông tin push lên lại là email công ty?
Câu trả lời rất đơn giản, nằm ở chỗ config của git mà thôi.
Git có 2 loại config chính cho các repo là Global config và Local config. Với những thông tin mà được sử dụng ở tất cả các repo thì thường sẽ config trong Global config. Còn những thông tin riêng của từng project sẽ được config trong Local config của từng dự án.
Tuy nhiên, rất nhiều developer quên, hoặc không biết đến điều này.
Cùng xem ví dụ bên dưới.
Ví dụ về config Git Global trong máy: (open terminal và gõ lệnh git config --list)
Git global config sample
Sau khi gõ lệnh thì ta sẽ lấy được tất cả thông tin mà git global config đang setting. (ấn q để thoát ra)
Ví dụ về 1 project không có config user name và email (dùng lệnh git config --list --local)
Với project/repo này thì khi commit lên, do thông tin user name và email bị thiếu, nêu git sẽ lấy config default trong Global config để thêm vào. Do đó, nếu bạn đã từng setting email global là email công ty, thì với commit này của bạn, email cũng sẽ là mail công ty, mặc dù repo này bạn không hề đăng ký bằng email công ty. Và IT sờ gáy.
Vậy, làm thế nào để config thông tin cho từng repo? Ví dụ như dưới
Và như vậy, config đã được áp dụng cho project của bạn, check lại bằng command git config --list --local, nếu 2 dòng cuối cùng hiển thị thông tin đúng như bạn đã config là thành công
Hãy hết sức cẩn thận khi commit nếu như bạn đang tham gia nhiều project khác nhau, sẽ không tốt chút nào nếu repo cá nhân lại bị gắn email công ty, và ngược lại.
I started working with Docker and Container almost 7 or 8 years ago, but I focused 2 last year on architecting on the Cloud. Docker, known for the motto Build, Ship, Run, changed the Software development world a lot, especially web-based.
Docker — Build, Ship, and Run
We know that we can run software with Docker everywhere, and we make the development and production environment closer and closer, but I have thought: Can we make the development environment between developer and developer closer with Docker, even unify?
I used Docker Compose in almost project; it’s worked well to make a database or messaging broker (Kafka) unified, but except for co-workers’ code, they tend to make the change locally and run it natively with their PC and Workstation, for e.g: run php artisan serve with Laravel or go build && go run with Golang. There are many pain points with many bugs because of so f**king many variables in their environment: Go Version, Php Version, Composer Version.
Visual Studio Code Remote — Containerswill solve this problem. I know about it at random. I am trying to fork WikiJS for internal use wiki in GST, and WikiJS teams are using Remote Container.
To run and update code, I have not to install specific required NPM, Database, Vue package, or even Elastic Search. It’s packed with a configured devcontainer.json and simple docker-compose.yml to define the environment and creating process.
devcontainer link with docker-compose.yml, specific service wiki will be chosen as the development environment, postCreateCommand will run right after containers are created in Docker Compose
We just need to install Remote Container extension in VSCode and reopen the current project in container view, Extension and Docker will do the rest.
Wait for the create container process to finish.Then we have a fully installed dev environment and are ready to make a new code.
Nearly every VCS has some form of branching support. Branching means you diverge from the main line of development and continue to do work without messing with that main line. In many VCS tools, this is a somewhat expensive process, often requiring you to create a new copy of your source code directory, which can take a long time for large projects.
Some people refer to the branching model in Git as its "killer feature," and it certainly sets Git apart in the VCS community. Why is it so special? The way Git branches is incredibly lightweight, making branching operations nearly instantaneous and switching back and forth between branches generally just as fast. Unlike many other VCSs, Git encourages a workflow that branches and merges often, even multiple times in a day. Understanding and mastering this feature gives you a powerful and unique tool and can literally change the way that you develop.
What a Branch Is
To really understand the way Git does branching, we need to take a step back and examine how Git stores its data. As you may remember from Chapter 1, Git doesn’t store data as a series of changesets or deltas, but instead as a series of snapshots.
When you commit in Git, Git stores a commit object that contains a pointer to the snapshotofthecontentyoustaged,theauthorandmessagemetadata,andzeroormore pointers to the commit or commits that were the direct parents of this commit: zero parents for the first commit, one parent for a normal commit, and multiple parents for a commit that results from a merge of two or more branches.
To visualize this, let’s assume that you have a directory containing three files, and youstagethemallandcommit. Stagingthefileschecksumseachone(theSHA–1hash we mentioned in Chapter 1), stores that version of the file in the Git repository (Git refers to them as blobs), and adds that checksum to the staging area:
$ git add README test.rb LICENSE2
$ git commit -m 'initial commit of my project'
When you create the commit by running git commit, Git checksums each subdirectory (in this case, just the root project directory) and stores those tree objects in the
Git repository. Git then creates a commit object that has the metadata and a pointer to the root project tree so it can re-create that snapshot when needed.
Your Git repository now contains five objects: one blob for the contents of each of your three files, one tree that lists the contents of the directory and specifies which file names are stored as which blobs, and one commit with the pointer to that root tree and all the commit metadata. Conceptually, the data in your Git repository looks something like Figure 3.1.
Figure 3.1: Single commit repository data
If you make some changes and commit again, the next commit stores a pointer to the commit that came im../mediately before it. After two more commits, your history might look something like Figure 3.2.
Figure 3.2: Git object data for multiple commits
A branch in Git is simply a lightweight movable pointer to one of these commits.The default branch name in Git is master. As you initially make commits, you’re given a master branch that points to the last commit you made. Every time you commit, it moves forward automatically.
What happens if you create a new branch? Well, doing so creates a new pointer for you to move around. Let’s say you create a new branch called testing. You do this with the git branch command:
$ git branch testing
Figure 3.3: Branch pointing into the commit data’s history
Figure 3.4: Multiple branches pointing into the commit’s data history
Figure 3.5: HEAD file pointing to the branch you’re on
To switch to an existing branch, you run the git checkout command. Let’s switch to the new testing branch:
$ git checkout testing
This moves HEAD to point to the testing branch (see Figure 3.6).
Figure 3.6: HEAD points to another branch when you switch branches.
What is the significance of that? Well, let’s do another commit:
$ vim test.rb $ git commit -a -m 'made a change'
Figure 3.7: The branch that HEAD points to moves forward with each commit.
This is interesting, because now your testing branch has moved forward, but your master branch still points to the commit you were on when you ran git checkout to switch branches. Let’s switch back to the master branch:
$ git checkout master
Figure 3.8: HEAD moves to another branch on a checkout.
Figure 3.8 shows the result. That command did two things. It moved the HEAD pointer back to point to the master branch, and it reverted the files in your working directory back to the snapshot that master points to. This also means the changes you make from this point forward will diverge from an older version of the project. It essentially rewinds the work you’ve done in your testing branch temporarily so you can go in a different direction.
Let’s make a few changes and commit again:
$ vim test.rb
$ git commit -a -m 'made other changes'
Now your project history has diverged (see Figure 3.9). You created and switched to a branch, did some work on it, and then switched back to your main branch and did other work. Both of those changes are isolated in separate branches: you can switch back and forth between the branches and merge them together when you’re ready. And you did all that with simple branch and checkout commands.
Figure 3.9: The branch histories have diverged.
Because a branch in Git is in actuality a simple file that contains the 40 character SHA–1 checksum of the commit it points to, branches are cheap to create and destroy. Creating a new branch is as quick and simple as writing 41 bytes to a file (40 characters
and a newline).
This is in sharp contrast to the way most VCS tools branch, which involves copying all of the project’s files into a second directory. This can take several seconds or even minutes, depending on the size of the project, whereas in Git the process is always instantaneous. Also, because we’re recording the parents when we commit, finding a proper merge base for merging is automatically done for us and is generally very easy to do. These features help encourage developers to create and use branches often.
Let’s see why you should do so.
Basic Branching and Merging
Let’s go through a simple example of branching and merging with a workflow that you might use in the real world. You’ll follow these steps:
Do work on a web site.
Create a branch for a new story you’re working on.
Do some work in that branch.
At this stage, you’ll receive a call that another issue is critical and you need a hotfix. You’ll do the following:
Revert back to your production branch.
Create a branch to add the hotfix.
After it’s tested, merge the hotfix branch, and push to production.
Switch back to your original story and continue working.
Basic Branching
First, let’s say you’re working on your project and have a couple of commits already (see Figure 3.10).
Figure 3.10: A short and simple commit history
You’ve decided that you’re going to work on issue #53 in whatever issue-tracking system your company uses. To be clear, Git isn’t tied into any particular issue-tracking system; but because issue #53 is a focused topic that you want to work on, you’ll create a new branch in which to work. To create a branch and switch to it at the same time, you can run the git checkout command with the -b switch:
$ git checkout -b iss53
Switched to a new branch "iss53"
This is shorthand for
$ git branch iss53
$ git checkout iss53
Figure 3.11: Creating a new branch pointer
You work on your web site and do some commits. Doing so moves the iss53 branch forward, because you have it checked out (that is, your HEAD is pointing to it; see Figure 3.12):
$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'
Figure 3.12: The iss53 branch has moved forward with your work.
Now you get the call that there is an issue with the web site, and you need to fix it im../mediately. im../mediately. With Git, you don’t have to deploy your fix along with the iss53 changes you’ve made, and you don’t have to put a lot of effort into reverting those changes before you can work on applying your fix to what is in production. All you have to do is switch back to your master branch.
However, before you do that, note that if your working directory or staging area has uncommitted changes that conflict with the branch you’re checking out, Git won’t let you switch branches. It’s best to have a clean working state when you switch branches. There are ways to get around this (namely, stashing and commit amending) that we’ll cover later. For now, you’ve committed all your changes, so you can switch back to your master branch:
$ git checkout master
Switched to branch "master"
At this point, your project working directory is exactly the way it was before you started working on issue #53, and you can concentrate on your hotfix. This is an important point to remember: Git resets your working directory to look like the snapshot of the commit that the branch you check out points to. It adds, removes, and modifies files automatically to make sure your working copy is what the branch looked like on your last commit to it.
Next, you have a hotfix to make. Let’s create a hotfix branch on which to work unti it’s completed (see Figure 3.13):
$ git checkout -b ’hotfix’
Switched to a new branch "hotfix"
$ vim index.html
$ git commit -a -m ’fixed the broken email address’
[hotfix]: created 3a0874c: "fixed the broken email address"
1 files changed, 0 insertions(+), 1 deletions(-)
Figure 3.13: hotfix branch based back at your master branch point
You can run your tests, make sure the hotfix is what you want, and merge it back into your master branch to deploy to production. You do this with the git merge command:
You’ll notice the phrase "Fast forward" in that merge. Because the commit pointed to by the branch you merged in was directly upstream of the commit you’re on, Git moves the pointer forward. To phrase that another way, when you try to merge one commit with a commit that can be reached by following the first commit’s history, Git simplifies things by moving the pointer forward because there is no divergent work to merge together — this is called a "fast forward".
Your change is now in the snapshot of the commit pointed to by the master branch, and you can deploy your change (see Figure 3.14).
After that your super-important fix is deployed, you’re ready to switch back to the work you were doing before you were interrupted. However, first you’ll delete the
Figure 3.14: Your master branch points to the same place as your hotfix branch after the merge.
hotfix branch, because you no longer need it — the master branch points at the same place. You can delete it with the -d option to git branch:
Now you can switch back to your work-in-progress branch on issue #53 and continue working on it (see Figure 3.15):
$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m ’finished the new footer [issue 53]’
[iss53]: created ad82d7a: "finished the new footer [issue 53]"
1 files changed, 1 insertions(+), 0 deletions(-)
Figure 3.15: Your iss5 branch can move forward independently.
It’s worth noting here that the work you did in your hotfix branch is not contained in the files in your iss53 branch. If you need to pull it in, you can merge your master branch into your iss53 branch by running git merge master, or you can wait to integrate those changes until you decide to pull the iss53 branch back into master later.
Basic Merging
Suppose you’ve decided that your issue #53 work is complete and ready to be merged into your master branch. In order to do that, you’ll merge in your iss53 branch, much like you merged in your hotfix branch earlier. All you have to do is check out the branch you wish to merge into and then run the git merge command:
This looks a bit different than the hotfix merge you did earlier. In this case, your development history has diverged from some older point. Because the commit on the branch you’re on isn’t a direct ancestor of the branch you’re merging in, Git has to do some work. In this case, Git does a simple three-way merge, using the two snapshots pointed to by the branch tips and the common ancestor of the two. Figure 3.16 highlights the three snapshots that Git uses to do its merge in this case.
Figure 3.16: Git automatically identifies the best common-ancestor merge base for branch merging.
Instead of just moving the branch pointer forward, Git creates a new snapshot that results from this three-way merge and automatically creates a new commit that points to it (see Figure 3.17). This is referred to as a merge commit and is special in that it has more than one parent. It’s worth pointing out that Git determines the best common ancestor to use for its merge base; this is different than CVS or Subversion (before version 1.5), where the developer doing the merge has to figure out the best merge base for themselves. This makes merging a heck of a lot easier in Git than in these other systems. Now that your work is merged in, you have no further need for the iss53 branch. You can delete it and then manually close the ticket in your ticket-tracking system:
Figure 3.17: Git automatically creates a new commit object that contains the merged work.
$ git branch -d iss53
Basic Merge Conflicts
Occasionally, this process doesn’t go smoothly. If you changed the same part of the same file differently in the two branches you’re merging together, Git won’t be able to merge them cleanly. If your fix for issue #53 modified the same part of a file as the hotfix, you’ll get a merge conflict that looks something like this:
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
Git hasn’t automatically created a new merge commit. It has paused the process while you resolve the conflict. If you want to see which files are unmerged at any point after a merge conflict, you can run git status:
[master*]$ git status index.html: needs merge
# On branch master
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# unmerged: index.html
#
Anything that has merge conflicts and hasn’t been resolved is listed as unmerged. Git adds standard conflict-resolution markers to the files that have conflicts, so you can open them manually and resolve those conflicts. Your file contains a section that looks something like this:
<<<<<<< HEAD:index.html
<div id="footer">contact : [email protected]</div>
=======
<div id="footer">
please contact us at [email protected]
</div> >>>>>>> iss53:index.html
This means the version in HEAD (your master branch, because that was what you had checked out when you ran your merge command) is the top part of that block (everything above the =======), while the version in your iss53 branch looks like everything in the bottom part. In order to resolve the conflict, you have to either choose one side or the other or merge the contents yourself. For instance, you might resolve this conflict by replacing the entire block with this:
This resolution has a little of each section, and I’ve fully removed the <<<<<<<, =======, and >>>>>>> lines. After you’ve resolved each of these sections in each conflicted file, run git add on each file to mark it as resolved. Staging the file marks it as resolved in Git. If you want to use a graphical tool to resolve these issues, you can run git mergetool, which fires up an appropriate visual merge tool and walks you through the conflicts:
$ git mergetool
merge tool candidates: kdiff3 tkdiff xxdiff meld gvimdiff opendiff emerge vimdiff
Merging the files: index.html
Normal merge conflict for ’index.html’:
{local}: modified
{remote}: modified
Hit return to start merge resolution tool (opendiff):
If you want to use a merge tool other than the default (Git chose opendiff for me in this case because I ran the command on a Mac), you can see all the supported tools listed at the top after "merge tool candidates". Type the name of the tool you’d rather use. In Chapter 7, we’ll discuss how you can change this default value for your environment.
After you exit the merge tool, Git asks you if the merge was successful. If you tell the script that it was, it stages the file to mark it as resolved for you.
You can run git status again to verify that all conflicts have been resolved:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: index.html
#
If you’re happy with that, and you verify that everything that had conflicts has been staged, you can type git commit to finalize the merge commit. The commit message by default looks something like this:
Merge branch 'iss53'
Conflicts:
index.html
#
# It looks like you may be committing a MERGE.
# If this is not correct, please remove the file
# .git/MERGE_HEAD
# and try again.
#
You can modify that message with details about how you resolved the merge if you think it would be helpful to others looking at this merge in the future — why you did what you did, if it’s not obvious.
Branch Management
Now that you’ve created, merged, and deleted some branches, let’s look at some branch management tools that will come in handy when you begin using branches all the time.
The git branch command does more than just create and delete branches. If you run it with no arguments, you get a simple listing of your current branches:
$ git branch
iss53
* master
testing
Notice the * character that prefixes the master branch: it indicates the branch that you currently have checked out. This means that if you commit at this point, the master branch will be moved forward with your new work. To see the last commit on each branch, you can run git branch -v:
$ git branch -v
iss53 93b412c fix javascript issue
* master 7a98805 Merge branch ’iss53’
testing 782fd34 add scott to the author list in the readmes
Another useful option to figure out what state your branches are in is to filter this list to branches that you have or have not yet merged into the branch you’re currently on. The useful --merged and --no-merged options have been available in Git since version 1.5.6 for this purpose. To see which branches are already merged into the branch you’re on, you can run git branch merged:
$ git branch --merged
iss53
* master
Because you already merged in iss53 earlier, you see it in your list. Branches on this list without the * in front of them are generally fine to delete with git branch -d; you’ve already incorporated their work into another branch, so you’re not going to lose anything.
To see all the branches that contain work you haven’t yet merged in, you can run git branch --no-merged:
$ git branch --no-merged
testing
This shows your other branch. Because it contains work that isn’t merged in yet, trying to delete it with git branch -d will fail:
$ git branch -d testing
error: The branch 'testing' is not an ancestor of your current HEAD.
If you are sure you want to delete it, run git branch -D testing. If you really do want to delete the branch and lose that work, you can force it with -D, as the helpful message points out.
Branching Work flows
Now that you have the basics of branching and merging down, what can or should you do with them? In this section, we’ll cover some common workflows that this lightweight branching makes possible, so you can decide if you would like to incorporate it into your own development cycle.
Long-Running Branches
Because Git uses a simple three-way merge, merging from one branch into another multiple times over a long period is generally easy to do. This means you can have several branches that are always open and that you use for different stages of your development cycle; you can merge regularly from some of them into others.
Many Git developers have a workflow that embraces this approach, such as having only code that is entirely stable in their master branch — possibly only code that has been or will be released. They have another parallel branch named develop or next that they work from or use to test stability — it isn’t necessarily always stable, but whenever it gets to a stable state, it can be merged into master. It’s used to pull in topic branches (short-lived branches, like your earlier iss53 branch) when they’re ready, to make sure they pass all the tests and don’t introduce bugs.
In reality, we’re talking about pointers moving up the line of commits you’re making. The stable branches are farther down the line in your commit history, and the bleeding-edge branches are farther up the history (see Figure 3.18).
Figure 3.18: More stable branches are generally farther down the commit history.
It’s generally easier to think about them as work silos, where sets of commits graduate to a more stable silo when they’re fully tested (see Figure 3.19).
You can keep doing this for several levels of stability. Some larger projects also havea proposed or pu (proposed updates) branch that has integrated branches that may not be ready to go into the next or master branch. The idea is that your branches are at various levels of stability; when they reach a more stable level, they’re merged into the branch above them. Again, having multiple long-running branches isn’t necessary, but it’s often helpful, especially when you’re dealing with very large or complex projects.
Figure 3.19: It may be helpful to think of your branches as silos.
Topic Branches
Topic branches, however, are useful in projects of any size. A topic branch is a short-lived branch that you create and use for a single particular feature or related work. This is something you’ve likely never done with a VCS before because it’s generally too expensive to create and merge branches. But in Git it’s common to create, work on, merge, and delete branches several times a day.
You saw this in the last section with the iss53 and hotfix branches you created. You did a few commits on them and deleted them directly after merging them into your main branch. This technique allows you to context-switch quickly and completely — because your work is separated into silos where all the changes in that branch have to do with that topic, it’s easier to see what has happened during code review and such. You can keep the changes there for minutes, days, or months, and merge them in when they’re ready, regardless of the order in which they were created or worked on.
Consider an example of doing some work (on master), branching off for an issue (iss91), working on it for a bit, branching off the second branch to try another way of handling the same thing (iss91v2), going back to your master branch and working there for a while, and then branching off there to do some work that you’re not sure is a good idea (dumbidea branch). Your commit history will look something like Figure 3.20.
Now, let’s say you decide you like the second solution to your issue best (iss91v2); and you showed the dumbidea branch to your coworkers, and it turns out to be genius. You can throw away the original iss91 branch (losing commits C5 and C6) and merge in the other two. Your history then looks like Figure 3.21.
It’s important to remember when you’re doing all this that these branches are completely local. When you’re branching and merging, everything is being done only in your Git repository — no server communication is happening.
Figure 3.20: Your commit history with multiple topic branches
Figure 3.21: Your history after merging in dumbidea and iss91v2
Remote Branches
Remote branches are references to the state of branches on your remote repositories. They’re local branches that you can’t move; they’re moved automatically whenever you do any network communication. Remote branches act as bookmarks to remind you where the branches on your remote repositories were the last time you connected to them.
They take the form (remote)/(branch). For instance, if you wanted to see what the master branch on your origin remote looked like as of the last time you communicated with it, you would check the origin/master branch. If you were working on an issue with a partner and they pushed up an iss53 branch, you might have your own local iss53 branch; but the branch on the server would point to the commit at origin/iss53.
This may be a bit confusing, so let’s look at an example. Let’s say you have a Git server on your network at git.ourcompany.com. If you clone from this, Git automatically names it origin for you, pulls down all its data, creates a pointer to where its master branch is, and names it origin/master locally; and you can’t move it. Git also gives you your own master branch starting at the same place as origin’s master branch, so you have something to work from (see Figure 3.22).
Figure 3.22: A Git clone gives you your own master branch and origin/master pointing to origin’s master branch.
If you do some work on your local master branch, and, in the meantime, someone else pushes to git.ourcompany.com and updates its master branch, then your histories move forward differently. Also, as long as you stay out of contact with your origin server, your origin/master pointer doesn’t move (see Figure 3.23).
To synchronize your work, you run a git fetch origin command. This command looks up which server origin is (in this case, it’s git.ourcompany.com), fetches any data from it that you don’t yet have, and updates your local database, moving your origin/master pointer to its new, more up-to-date position (see Figure 3.24).
To demonstrate having multiple remote servers and what remote branches for those remote projects look like, let’s assume you have another internal Git server that is used only for development by one of your sprint teams. This server is at git.team1.ourcompany.com. You can add it as a new remote reference to the project you’re currently working on by running the git remote add command as we covered in Chapter 2. Name this remote teamone, which will be your shortname for that whole URL (see Figure 3.25).
Figure 3.23: Working locally and having someone push to your remote server makes each history move forward differently.
Figure 3.24: The git fetch command updates your remote references.
Now, you can run git fetch teamone to fetch everything server has that you don’t have yet. Because that server is a subset of the data your origin server has right now, Git fetches no data but sets a remote branch called teamone/master to point to the commit that teamone has as its master branch (see Figure 3.26).
Figure 3.25: Adding another server as a remote
Figure 3.26: You get a reference to teamone’s master branch position locally.
Pushing
When you want to share a branch with the world, you need to push it up to a remote that you have write access to. Your local branches aren’t automatically synchronized to the remotes you write to — you have to explicitly push the branches you want to share. That way, you can use private branches do work you don’t want to share, and push up only the topic branches you want to collaborate on.
If you have a branch named serverfix that you want to work on with others, you can push it up the same way you pushed your first branch. Run git push (remote) (branch):
This is a bit of a shortcut. Git automatically expands the serverfix branchname out to refs/heads/serverfix:refs/heads/serverfix, which means, "Take my serverfix local branch and push it to update the remote’s serverfix branch." We’ll go over the refs/heads/ part in detail in Chapter 9, but you can generally leave it off. You can also do git push origin serverfix:serverfix, which does the same thing — it says, "Take my serverfix and make it the remote’s serverfix." You can use this format to push a local branch into a remote branch that is named differently. If you didn’t want it to be called serverfix on the remote, you could instead run git push origin serverfix:awesomebranch to push your local serverfix branch to the awesomebranch branch on the remote project.
The next time one of your collaborators fetches from the server, they will get a reference to where the server’s version of serverfix is under the remote branch origin/serverfix:
It’s important to note that when you do a fetch that brings down new remote branches, you don’t automatically have local, editable copies of them. In other words, inthiscase,you don’t have a new serverfix branch — you only have an origin/serverfix pointer that you can’t modify.
To merge this work into your current working branch, you can run git merge origin/serverfix. If you want your own serverfix branch that you can work on, you can base it off your remote branch:
$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "serverfix"
This gives you a local branch that you can work on that starts where origin/serverfix is.
Tracking Branches
Checking out a local branch from a remote branch automatically creates what is called a tracking branch. Tracking branches are local branches that have a direct relationship to a remote branch. If you’re on a tracking branch and type git push, Git automatically knows which server and branch to push to. Also, running git pull while on one of these branches fetches all the remote references and then automatically merges in the corresponding remote branch.
When you clone a repository, it generally automatically creates a master branch that tracks origin/master. That’s why git push and git pull work out of the box with no other arguments. However, you can set up other tracking branches if you wish — ones that don’t track branches on origin and don’t track the master branch. The simple case is the example you just saw, running git checkout -b [branch][remotename]/[branch]. If you have Git version 1.6.2 or later, you can also use the –track shorthand:
$ git checkout --track origin/serverfix Branch serverfix set up to track remote branch refs/remotes/origin/serverfix. Switched to a new branch "serverfix"
To set up a local branch with a different name than the remote branch, you can easily use the first version with a different local branch name:
$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch refs/remotes/origin/serverfix.
Switched to a new branch "sf"
Now, your local branch sf will automatically push to and pull from origin/serverfix.
Deleting Remote Branches
Suppose you’re done with a remote branch — say, you and your collaborators are finished with a feature and have merged it into your remote’s master branch (or whatever branch your stable codeline is in). You can delete a remote branch using the rather obtuse syntax git push [remotename] :[branch]. If you want to delete your serverfix branch from the server, you run the following:
Boom. No more branch on your server. You may want to dog – ear this page, because you’ll need that command,and you’ll likely forget the syntax. Away to remember this command is by recalling the git push [remotename] [localbranch]:[remotebranch] syntax that we went over a bit earlier. If you leave off the [localbranch] portion, then you’re basically saying, "Take nothing on my side and make it be [remotebranch]."
Rebasing
In Git, there are two main ways to integrate changes from one branch into another: the merge and the rebase. In this section you’ll learn what rebasing is, how to do it, why it’s a pretty amazing tool, and in what cases you won’t want to use it.
Figure 3.27: Your initial diverged commit history
The Basic Rebase
If you go back to an earlier example from the Merge section (see Figure 3.27), you can see that you diverged your work and made commits on two different branches.
The easiest way to integrate the branches, as we’ve already covered, is the merge command. It performs a three-way merge between the two latest branch snapshots (C3 and C4) and the most recent common ancestor of the two (C2), creating a new snapshot (and commit), as shown in Figure 3.28.
Figure 3.28: Merging a branch to integrate the diverged work history
However, there is another way: you can take the patch of the change that was introduced in C3 and reapply it on top of C4. In Git, this is called rebasing. With the rebase command, you can take all the changes that were committed on one branch and replay them on another one. In this example, you’d run the following:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
It works by going to the common ancestor of the two branches (the one you’re on and the one you’re rebasing onto), getting the diff introduced by each commit of the branch you’re on, saving those diffs to temporary files, resetting the current branch to the same commit as the branch you are rebasing onto, and finally applying each change in turn. Figure 3.29 illustrates this process.
Figure 3.29: Rebasing the change introduced in C3 onto C4
At this point, you can go back to the master branch and do a fast-forward merge (see Figure 3.30).
Figure 3.30: Fast-forwarding the master branch
Now, the snapshot pointedto by C3 is exactly thesame as theone that waspointed to by C5 in the merge example. There is no difference in the end product of the integration, but rebasing makes for a cleaner history. If you examine the log of a rebased branch, it looks like a linear history: it appears that all the work happened in series, even when it originally happened in parallel.
Often, you’ll do this to make sure your commits apply cleanly on a remote branch — perhaps in a project to which you’re trying to contribute but that you don’t maintain. In this case, you’d do your work in a branch and then rebase your work onto origin/master when you were ready to submit your patches to the main project. That way, the maintainer doesn’t have to do any integration work — just a fast-forward or a clean apply.
Note that the snapshot pointed to by the final commit you end up with, whether it’s the last of the rebased commits for a rebase or the final merge commit after a merge, is the same snapshot — it’s only the history that is different. Rebasing replays changes from one line of work onto another in the order they were introduced, whereas merging takes the endpoints and merges them together.
More Interesting Rebases
You can also have your rebase replay on something other than the rebase branch. Take a history like Figure 3.31, for example. You branched a topic branch (server) to add some server-side functionality to your project, and made a commit. Then, you branched off that to make the client-side changes (client) and committed a few times. Finally, you went back to your server branch and did a few more commits.
Figure 3.31: A history with a topic branch off another topic branch
Suppose you decide that you want to merge your client-side changes into your mainline for a release, but you want to hold off on the server-side changes until it’s tested further. You can take the changes on client that aren’t on server (C8 and C9) and replay them on your master branch by using the --onto option of git rebase:
$ git rebase --onto master server client
This basically says, "Check out the client branch, figure out the patches from the common ancestor of the client and server branches, and then replay them onto master." It’s a bit complex; but the result, shown in Figure 3.32, is pretty cool.
Figure 3.32: Rebasing a topic branch off another topic branch
Now you can fast-forward your master branch (see Figure 3.33):
$ git checkout master
$ git merge client
Figure 3.33: Fast-forwarding your master branch to include the client branch changes
Then, you can fast-forward the base branch (master):
$ git checkout master
$ git merge server
You can remove the client and server branches because all the work is integrated and you don’t need them anymore, leaving your history for this entire process looking like Figure 3.35:
$ git branch -d client
$ git branch -d server
Figure 3.35: Final commit history
The Perils of Rebasing
Ahh, but the bliss of rebasing isn’t without its drawbacks, which can be summed up in a single line:
Do not rebase commits that you have pushed to a public repository.
If you follow that guideline, you’ll be fine. If you don’t, people will hate you, and you’ll be scorned by friends and family.
When you rebase stuff, you’re abandoning existing commits and creating new ones that are similar but different. If you push commits somewhere and others pull them down and base work on them, and then you rewrite those commits with git rebase and push them up again, your collaborators will have to re-merge their work and things will get messy when you try to pull their work back into yours.
Let’s look at an example of how rebasing work that you’ve made public can cause problems. Suppose you clone from a central server and then do some work off that. Your commit history looks like Figure 3.36.
Figure 3.36: Clone a repository, and base some work on it.
Now, someone else does more work that includes a merge, and pushes that work to the central server. You fetch them and merge the new remote branch into your work, making your history look something like Figure 3.37.
Next, the person who pushed the merged work decides to go back and rebase their work instead; they do a git push --force to overwrite the history on the server. You then fetch from that server, bringing down the new commits.
At this point, you have to merge this work in again, even though you’ve already done so. Rebasing changes the SHA–1 hashes of these commits so to Git they look like new commits, when in fact you already have the C4 work in your history (see Figure 3.39).
You have to merge that work in at some point so you can keep up with the other developer in the future. After you do that, your commit history will contain both the C4 and C4’ commits, which have different SHA–1 hashes but introduce the same work and have the same commit message. If you run a git log when your history looks like this, you’ll see two commits that have the same author date and message, which will be confusing. Furthermore, if you push this history back up to the server, you’ll reintroduce all those rebased commits to the central server, which can further confuse people.
Figure 3.37: Fetch more commits, and merge them into your work.
Figure 3.38: Someone pushes rebased commits, abandoning commits you’ve based your work on.
If you treat rebasing as a way to clean up and work with commits before you push them, and if you only rebase commits that have never been available publicly, then you’ll be fine. If you rebase commits that have already been pushed publicly, and people may have based work on those commits, then you may be in for some frustrating trouble.
Figure 3.39: You merge in the same work again into a new merge commit.
Summary
We’ve covered basic branching and merging in Git. You should feel comfortable creating and switching to new branches, switching between branches and merging local branches together. You should also be able to share your branches by pushing them to a shared server, working with others on shared branches and rebasing your branches before they are shared.
You can get a Git project using two main approaches. The first takes an existing project or directory and imports it into Git. The second clones an existing Git repository from another server.
Initializing a Repository in an Existing Directory
If you’re starting to track an existing project in Git, you need to go to the project’s directory and type
$ git init
This creates a new sub directory named .git that contains all of your necessary repository files — a Git repository skeleton. At this point, nothing in your project is tracked yet. (See Chapter 9 for more information about exactly what files are contained in the .git directory you just created.)
If you want to start version-controlling existing files (as opposed to an empty directory), you should probably begin tracking those files and do an initial commit. You can accomplish that with a few git add
commands that specify the files you want to track, followed by a commit:
We’ll go over what these commands do in just a minute. At this point, you have a Git repository with tracked files and an initial commit.
Cloning an Existing Repository
If you want to get a copy of an existing Git repository — for example, a project you’d like to contribute to — the command you need is git clone. If you’re familiar with other VCS systems such as Subversion, you’ll notice that the command is clone and not checkout. This is an important distinction — Git receives a copy of nearly all data that the server has. Every version of every file for the history of the project is pulled down when you run git clone. In fact, if your server disk gets corrupted, you can use any of the clones on any client to set the server back to the state it was in when it was cloned (you may lose some server-side hooks and such, but all the versioned data would be there—see Chapter 4 for more details). You clone a repository with git clone [url]. For example, if you want to clone the Ruby Git library called Grit, you can do so like this:
$ git clone git://github.com/wingadium1/grit.git
That creates a directory named grit, initializes a .git directory inside it, pulls down all the data for that repository, and checks out a working copy of the latest version. If you go into the new grit directory, you’ll see the project files in there, ready to be worked on or used. If you want to clone the repository into a directory named something other than grit, you can specify that as the next command-line option:
That command does the same thing as the previous one, but the target directory is called mygrit. Git has a number of different transfer protocols you can use. The previous example uses the git:// protocol, but you may also see http(s):// or user@server:/path.git, which uses the SSH transfer protocol. Chapter 4 will introduce all of the available options the server can set up to access your Git repository and the pros and cons of each.
Recording Changes to the Repository
You have a bona fide Git repository and a checkout or working copy of the files for that project. You need to make some changes and commit snapshots of those changes into your repository each time the project reaches a state you want to record.
Remember that each file in your working directory can be in one of two states: tracked or untracked. Tracked files are files that were in the last snapshot; they can be unmodified, modified, or staged. Untracked files are everything else – any files in your working directory that were not in your last snapshot and are not in your staging area. When you first clone a repository, all of your files will be tracked and unmodified because you just checked them out and haven’t edited anything.
As you edit files, Git sees them as modified, because you’ve changed them since your last commit. You stage these modified files and then commit all your staged changes, and the cycle repeats. This lifecycle is illustrated in Figure 2.1.
Figure 2.1 The lifecycle of the status of your files
Checking the Status of Your Files
The main tool you use to determine which files are in which state is the git status command. If you run this command directly after a clone, you should see something like this:
$ git status
#On branch master nothing to commit (working directory clean)
This means you have a clean working directory—in other words, there are no tracked and modified files. Git also doesn’t see any untracked files, or they would be listed here. Finally, the command tells you which branch you’re on. For now, that is always master, which is the default; you won’t worry about it there. Then next chapter will go over branches and references in detail. Let’s say you add a new file to your project, a simple README file. If the file didn’t exist before, and you run git status, you see your untracked file like so:
$ vim README
$ git status
# On branch master
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# README
nothing added to commit but untracked files present (use "git add" to track)
You can see that your new README file is untracked, because it’s under the "Untracked files" heading in your status output. Untracked basically means that Git sees a file you didn’t have in the previous snapshot (commit); Git won’t start including it in your commit snapshots until you explicitly tell it to do so. It does this so you don’t accidentally begin including generated binary files or other files that you did not mean to include. You do want to start including README, so let’s start tracking the file.
Tracking New Files
In order to begin tracking a new file, you use the command git add. To begin tracking the README file, you can run this:
$ git add README
If you run your status command again, you can see that your README file is now tracked and staged:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
#
You can tell that it’s staged because it’s under the "Changes to be committed" heading. If you commit at this point, the version of the file at the time you ran git add is what will be in the historical snapshot. You may recall that when you ran git init earlier, you then ran git add (files) — that was to begin tracking files in your directory. The git add command takes a path name for either a file or a directory; if it’s a directory, the command adds all the files in that directory recursively.
Staging Modified Files
Let’s change a file that was already tracked. If you change a previously tracked file called benchmarks.rb and then run your status command again, you get something that looks like this:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: benchmarks.rb
#
The benchmarks.rb file appears under a section named "Changed but not updated" — which means that a file that is tracked has been modified in the working directory but not yet staged. To stage it, you run the git add command (it’s a multipurpose command—you use it to begin tracking new files, to stage files, and to do other things like marking merge-conflicted files as resolved). Let’s run git add now to stage the benchmarks.rb file, and then run git status again:
$ git add benchmarks.rb
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
# modified: benchmarks.rb
#
Both files are staged and will go into your next commit. At this point, suppose you remember one little change that you want to make in benchmarks.rb before you commit it. You open it again and make that change, and you’re ready to commit. However, let’s run git status one more time:
$ vim benchmarks.rb
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
# modified: benchmarks.rb
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: benchmarks.rb
#
What the heck? Now benchmarks.rb is listed as both staged and unstaged. How is that possible? It turns out that Git stages a file exactly as it is when you run the git add command. If you commit now, the version of benchmarks.rb as it was when you last ran the git add command is how it will go into the commit, not the version of the file as it looks in your working directory when you run git commit. If you modify a file after you run git add, you have to run git add again to stage the latest version of the file:
$ git add benchmarks.rb
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
# modified: benchmarks.rb
#
Ignoring Files
Often, you’ll have a class of files that you don’t want Git to automatically add or even show you as being untracked. These are generally automatically generated files such as log files or files produced by your build system. In such cases, you can create a file listing patterns to match them named .gitignore. Here is an example .gitignore file:
$ cat .gitignore *.[oa] *˜
The first line tells Git to ignore any files ending in .o or .a — object and archive files that may be the product of building your code. The second line tells Git to ignore all files that end with a tilde ( ), which is used by many text editors such as Emacs to mark temporary files. You may also include a log, tmp, or pid directory; automatically generated documentation; and soon. Setting up a .gitignore file before you get going is generally a good idea so you don’t accidentally commit files that you really don’t want in your Git repository. The rules for the patterns you can put in the .gitignore file are as follows:
Blank lines or lines starting with # are ignored.
Standard glob patterns work.
You can end patterns with a forward slash (/) to specify a directory.
You can negate a pattern by starting it with an exclamation point (!).
Glob patterns are like simplified regular expressions that shells use. An asterisk (*) matches zero or more characters; [abc] matches any character inside the brackets (in this case a, b, or c); a question mark (?) matches a single character; and brackets enclosing characters seperated by a hyphen([0-9]) matches any character between them (in this case 0 through 9).
Here is another example .gitignore file:
# a comment this is ignored
*.a
# no .a files
!lib.a
# but do track lib.a, even though you're ignoring .a files above
/TODO
# only ignore the root TODO file, not subdir/TODO
build/
# ignore all files in the build/ directory
doc/*.txt
# ignore doc/notes.txt, but not doc/server/arch.txt
Viewing Your Staged and Unstaged Changes
If the git status command is too vague for you — you want to know exactly what youchanged,notjustwhichfileswerechanged—youcanusethegit diffcommand. We’ll cover git diff in more detail later; but you’ll probably use it most often to answer these two questions: What have you changed but not yet staged? And what have you staged that you are about to commit? Although git status answers those questions very generally, git diff shows you the exact lines added and removed — the patch, as it were.
Let’s say you edit and stage the README file again and then edit the benchmarks.rb file without staging it. If you run your status command, you once again see something like this:
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: benchmarks.rb #
To see what you’ve changed but not yet staged, type git diff with no other arguments:
$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..da65585 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
@commit.parents[0].parents[0].parents[0]
end
+ run_code(x, 'commits 1') do
+ git.commits.size
+ end
+ run_code(x, 'commits 2') do
log = git.commits('master', 15)
log.size
That command compares what is in your working directory with what is in your staging area. The result tells you the changes you’ve made that you haven’t yet staged.
If you want to see what you’ve staged that will go into your next commit, you can use git diff -cached. (In Git versions 1.6.1 and later, you can also use git diff -staged, which may be easier to remember.) This command compares your staged changes to your last commit:
$ git diff --cached
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
+++ b/README2
@@ -0,0 +1,5 @@
+ grit
+ by Tom Preston-Werner, Chris Wanstrath
+ http://github.com/mojombo/grit
+
+ Grit is a Ruby library for extracting information from a Git repository
It’s important to note that git diff by itself doesn’t show all changes made since your last commit — only changes that are still unstaged. This can be confusing, because if you’ve staged all of your changes, git diff will give you no output.
For another example, if you stage the benchmarks.rb file and then edit it, you can use git diff to see the changes in the file that are staged and the changes that are unstaged:
$ git add benchmarks.rb
$ echo '# test line' >> benchmarks.rb
$ git status
# On branch master
#
# Changes to be committed:
#
# modified: benchmarks.rb
#
# Changed but not updated:
#
# modified: benchmarks.rb
#
Now you can use git diff to see what is still unstaged
$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index e445e28..86b2f7c 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -127,3 +127,4 @@ end
main()
##pp Grit::GitRuby.cache_client.stats
+
# test line
and git diff –cached to see what youve staged so far:
$ git diff --cached
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..e445e28 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
@commit.parents[0].parents[0].parents[0]
end
+ run_code(x, 'commits 1') do
+ git.commits.size
+ end
+ run_code(x, 'commits 2')
do log = git.commits('master', 15)
log.size
Committing Your Changes
Now that your staging area is setup the way you want it, you can commit your changes. Remember that any thing that is still unstaged — any files you have created or modified that you haven’t run git add on since you edited them — won’t go into this commit.
They will stay as modified files on your disk. In this case, the last time you ran git status, you saw that everything was staged, so you’re ready to commit your changes. The simplest way to commit is to type git commit:
$ git commit
Doing so launches your editor of choice. (This is set by your shell’s $EDITOR environment variable — usually vim or emacs, although you can configure it with whatever you want using the git config --global core.editor command as you saw in Chapter 1.
The editor displays the following text (this example is a Vim screen):
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: README
# modified: benchmarks.rb
˜
˜
˜
".git/COMMIT_EDITMSG" 10L, 283C
You can see that the default commit message contains the latest output of the git status command commented out and one empty line on top. You can remove these comments and type your commit message, or you can leave them there to help you remember what you’re committing. (For a nevenmore explicit reminder of what you’ve modified, you can pass the -v option to git commit. Doing so also puts the diff of your change in the editor so you can see exactly what you did.) When you exit the editor, Git creates your commit with that commit message (with the comments and diff stripped out).
Alternatively, you can type your commit message inline with the commit command by specifying it after a -m flag, like this:
$ git commit -m "Story 182: Fix benchmarks for speed"
[master]: created 463dc4f: "Fix benchmarks for speed"
2 files changed, 3 insertions(+), 0 deletions(-)
create mode 100644 README
Now you’ve created your first commit! You can see that the commit has given you some output about itself: which branch you committed to (master), what SHA–1 checksumthecommithas(463dc4f),howmanyfileswerechanged,andstatisticsabout lines added and removed in the commit.
Remember that the commit records the snapshot you set up in your staging area. Anything you didn’t stage is still sitting there modified; you can do another commit to add it to your history. Every time you perform a commit, you’re recording a snapshot of your project that you can revert to or compare to later.
Skipping the Staging Area
Although it can be amazingly useful for crafting commits exactly how you want them, the staging area is sometimes a bit more complex than you need in your workflow. If you want to skip the staging area, Git provides a simple shortcut. Providing the -a option to the git commit command makes Git automatically stage every file that is already tracked before doing the commit, letting you skip the git add part:
$ git status
# On branch master
#
# Changed but not updated:
#
# modified: benchmarks.rb
#
$ git commit -a -m 'added new benchmarks'
[master 83e38c7] added new benchmarks
1 files changed, 5 insertions(+),
0 deletions(-)
Notice how you don’t have to run git add on the benchmarks.rb file in this case before you commit.
RemovingFiles
To remove a file from Git, you have to remove it from your tracked files (more accurately, remove it from your staging area) and then commit. The git rm command does that and also removes the file from your working directory so you don’t see it as an untracked file next time around.
If you simply remove the file from your working directory, it shows up under the "Changed but not updated" (that is, unstaged) area of your git status output:
$ rm grit.gemspec $ git status
# On branch master
#
# Changed but not updated:
# (use "git add/rm <file>..." to update what will be committed)
#
# deleted: grit.gemspec
#
Then, if you run git rm, it stages the file’s removal:
$ git rm grit.gemspec rm 'grit.gemspec' $ git status
# On branch master
#
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# deleted: grit.gemspec
#
The next time you commit, the file will be gone and no longer tracked. If you modified the file and added it to the index already,you must force the removal with the -f option. This is a safety feature to prevent accidental removal of data that hasn’t yet been recorded in a snapshot and that can’t be recovered from Git.
Another useful thing you may want to do is to keep the file in your working tree but remove it from your staging area. In other words, you may want to keep the file on your hard drive but not have Git track it anymore. This is particularly useful if you forgot to add something to your .gitignore file and accidentally added it, like a large log file or a bunch of .a compiled files. To do this, use the --cached option:
$ git rm --cached readme.txt
You can pass files, directories, and file-glob patterns to the git rm command. That means you can do things such as
$ git rm log/\*.log
Note the backslash () in front of the *. This is necessary because Git does its own file name expansion in addition to your shell’s file name expansion. This command removes all files that have the .log extension in the log/ directory. Or, you can do something like this:
$ git rm \*.txt
This command removes all files that end with .txt
Moving Files
Unlike many other VCS systems, Git doesn’t explicitly track file movement. If you rename a file in Git, no metadata is stored in Git that tells it you renamed the file. However, Git is pretty smart about figuring that out after the fact — we’ll deal with detecting file movement a bit later. Thus it’s a bit confusing that Git has a mv command. If you want to rename a file in Git, you can run something like
$ git mv file_from file_to
and it works fine. In fact, if you run something like this and look at the status, you’ll see that Git considers it a renamed file:
$ git mv README.txt README $ git status
# On branch master
# Your branch is ahead of 'origin/master' by 1 commit.
#
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# renamed: README.txt -> README #
However, this is equivalent to running something like this:
Git figures out that it’s are name implicitly, so it doesn’t matter if you rename a file that way or with the mv command. The only real difference is that mv is one command instead of three — it’s a convenience function. More important, you can use any tool you like to rename a file, and address the add/rm later, before you commit.
Viewing the Commit History
After you have created several commits, or if you have cloned a repository with an existing commit history, you’ll probably want to look back to see what has happened. The most basic and powerful tool to do this is the git log command.
These examples use a very simple project called simplegit that I often use for demonstrations. To get the project, run
When you run git log in this project, you should get output that looks something like this:
$ git log
commit ca82a6dff817ec66f44342007202690a93763949
Author: Hoang Thanh Son <[email protected]>
Date: Mon Mar 17 21:52:11 2008 -0700
changed the verison number
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Hoang Thanh Son <[email protected]>
Date: Sat Mar 15 16:40:33 2008 -0700
removed unnecessary test code
commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Hoang Thanh Son <[email protected]>
Date: Sat Mar 15 10:31:28 2008 -0700
first commit
By default, with no arguments, git log lists the commits made in that repository in reverse chronological order. That is, the most recent commits show up first. As you cansee, this command lists each commit with its SHA–1 checksum, the author’s name and e-mail, the date written, and the commit message. A huge number and variety of options to the git log command are available to show you exactly what you’re looking for. Here, we’ll show you some of the mostused options. One of the more helpful options is -p, which shows the diff introduced in each commit. You can also use -2, which limits the output to only the last two entries:
$ git log p -2
commit ca82a6dff817ec66f44342007202690a93763949
Author: Hoang Thanh Son <[email protected]>
Date: Mon Mar 17 21:52:11 2008 -0700
changed the verison number
diff --git a/Rakefile b/Rakefile
index a874b73..8f94139 100644
--- a/Rakefile
+++ b/Rakefile
@@ -5,7 +5,7 @@ require 'rake/gempackagetask'
spec = Gem::Specification.new do |s|
- s.version = "0.1.0"
+ s.version = "0.1.1"
s.author = "Hoang Thanh Son"
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Hoang Thanh Son <[email protected]>
Date: Sat Mar 15 16:40:33 2008 -0700
removed unnecessary test code
diff --git a/lib/simplegit.rb b/lib/simplegit.rb
index a0a60ae..47c6340 100644
--- a/lib/simplegit.rb
+++ b/lib/simplegit.rb @@ -18,8 +18,3 @@ class SimpleGit
end
end
-if $0 == __FILE__
- git = SimpleGit.new
- puts git.show
-end
\ No newline at end of file
This option displays the same information but with a diff directly following each entry. This is very helpful for code review or to quickly browse what happened during a series of commits that a collaborator has added. You can also use a series of summarizing options with git log. For example, if you want to see some abbreviated stats for each commit, you can use the --stat option:
$ git log --stat
commit ca82a6dff817ec66f44342007202690a93763949
Author: Hoang Thanh Son <[email protected]>
Date: Mon Mar 17 21:52:11 2008 -0700
changed the verison number
Rakefile | 2 +1
files changed, 1 insertions(+), 1 deletions(-)
commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Hoang Thanh Son <[email protected]>
Date: Sat Mar 15 16:40:33 2008 -0700
removed unnecessary test code
lib/simplegit.rb | 5 ----
1 files changed, 0 insertions(+), 5 deletions(-)
commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Hoang Thanh Son <[email protected]>
Date: Sat Mar 15 10:31:28 2008 -0700
first commit
README | 6 ++++++
Rakefile | 23 +++++++++++++++++++++++
lib/simplegit.rb | 25 +++++++++++++++++++++++++
3 files changed, 54 insertions(+), 0 deletions(-)
As you can see,the –stat option prints below each commit entry a list of modified files, how many files were changed, and how many lines in those files were added and removed. It also puts a summary of the information at the end. Another really useful option is –pretty. This option changes the log output to formats other than the default. A few prebuilt options are available for you to use. The oneline option prints each commit on a single line, which is useful if you’re looking at a lot of commits. In addition, the short, full, and fuller options show the output in roughly the same format but with less or more information, respectively:
$ git log --pretty=oneline
ca82a6dff817ec66f44342007202690a93763949 changed the verison number
085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 removed unnecessary test code
a11bef06a3f659402fe7563abf99ad00de2209e6 first commit
The most interesting option is format, which allows you to specify your own log output format. This is especially useful when you’re generating output for machine parsing — because you specify the format explicitly, you know it won’t change with updates to Git:
$ git log --pretty=format:"%h - %an, %ar : %s"
ca82a6d - Hoang Thanh Son, 11 months ago : changed the verison number
085bb3b - Hoang Thanh Son, 11 months ago : removed unnecessary test code
a11bef0 - Hoang Thanh Son, 11 months ago : first commit
Option
Description of Output
%H
Commit hash
%h
Abbreviated commit hash
%T
Tree hash
%t
Abbreviated tree hash
%P
Parent hashes
%p
Abbreviated parent hashes
%an
Author name
%ae
Author e-mail
%ad
Author date (format respects the date= option)
%ar
Author date, relative
%cn
Committer name
%ce
Committer email
%cd
Committer date
%cr
Committer date, relative
%s
Subject
Table 2.1 Lists some of the more useful options that format takes.
You may be wondering what the difference is between author and committer. The author is the person who originally wrote the work,where as the committer is the person who last applied the work. So, if you send in a patch to a project and one of the core members applies the patch, both of you get credit — you as the author and the core member as the committer. We’ll cover this distinction a bit more in Chapter 5.
The oneline and format options are particularly useful with another log option called –graph. This option adds a nice little ASCII graph showing your branch and merge history, which we can see our copy of the Grit project repository:
$ git log --pretty=format:"%h %s" --graph
* 2d3acf9 ignore errors from SIGCHLD on trap
* 5e3ee11 Merge branch 'master' of git://github.com/dustin/grit
|\
| * 420eac9 Added a method for getting the current branch.
* | 30e367c timeout code and tests
* | 5a09431 add timeout protection to grit
* | e1193f8 support for heads with slashes in them
|/
* d6016bc require time for xmlschema
* 11d191e Merge branch 'defunkt' into local
Those are only some simple output-formatting options to git log — there are many more.
Option
Description
-p
Show the patch introduced with each commit.
–stat
Show statistics for files modified in each commit.
–shortstat
Display only the changed /insertions/deletions line from the –stat command.
–name-only
Show the list of files modified after the commit information.
–name-status
Show the list of files affected with added/modified/deleted information as well.
–abbrev-commit
Show only the first few characters of the SHA-1 checksum instead of all 40.
–relative-date
Display the date in a relative format (forexample,"2weeks ago") instead of using the full date format.
–graph
Display an ASCII graph of the branch and merge history beside the log output.
–pretty
Show commits in an alternate format. Options include one line, short, full, fuller, and format (where you specify your own format).
Table 2.2 Lists the options we’ve covereds of a randsome other common formatting options that may be useful, along with how they change the output of the log command.
Limiting Log Output
In addition to output-formatting options, git log takes a number of useful limiting options — that is, options that let you show only a subset of commits. You’ve seen one such option already — the -2 option, which show only the last two commits. In fact, you can do -<n>, where n is any integer to show the last n commits. In reality, you’re unlikely to use that often, because Git by default pipes all output through a pager so you see only one page of log output at a time.
However, the time-limiting options such as --since and --until are very useful. For example, this command gets the list of commits made in the last two weeks:
$ git log --since=2.weeks
This command works with lots of formats — you can specify a specific date ("2008– 01–15") or a relative date such as "2 years 1 day 3 minutes ago". You can also filter the list to commits that match some search criteria. The --author option allows you to filter on a specific author, and the --grep option lets you search for keywords in the commit messages. (Note that if you want to specify both author and grep options, you have to add --all-match or the command will match commits with either.) The last really useful option to pass to git log as a filter is a path. If you specify a directoryorfilename,you can limit the log output to commits that introduced a change to those files. This is always the last option an disgenerally preceded by double dashes (--) to separate the paths from the options. In Table 2.3 we’ll list these and a few other common options for your reference.
Option
Description
-(n)
Show only the last n commits
–since, –after
Limit the commits to those made after the specified date.
–until, –before
Limit the commits to those made before the specified date.
–author
Only show commits in which the author entry matches the specified string.
–committer
Only show commits in which the committer entry matches the specified string.
For example,if you want to see which commits modifying test files in the Git source code history were committed by Junio Hamano and were not merges in the month of October 2008, you can run something like this:
Of the nearly 20,000 commits in the Git source code history, this command shows the 6 that match those criteria.
Using a GUI to Visualize History
If you like to use a more graphical tool to visualize your commit history, you may want to take a look at a Tcl/Tk program called gitk that is distributed with Git. Gitk is basically a visual git log tool, and it accepts nearly all the filtering options that git log does. If you type gitk on the command line in your project, you should see something like Figure 2.2.
Figure 2.2: The gitk history visualizer
You can see the commit history in the top half of the window along with a nice ancestry graph. The diff viewer in the bottom half of the window shows you the changes introduced at any commit you click.
UndoingThings
At any stage, you may want to undo something. Here,we’ll review a few basic tools for undoing changes that you’ve made. Be careful, because you can’t always undo some of these undos. This is one of the few areas in Git where you may lose some work if you do it wrong.
Changing Your Last Commit
One of the common undos takes place when you commit too early and possibly forget to add some files, or you messup your commitmessage. If you want to try that commit again, you can run commit with the –amend option:
$ git commit --amend
This command takes your staging area and uses it for the commit. If you’ve have made no changes since your last commit (for instance, you run this command it im../mediately after your previous commit), then your snapshot will look exactly the same and all you’ll change is your commit message.
The same commit-message editor fires up, but it already contains the message of your previous commit. You can edit the message the same as always, but it overwrites your previous commit.
As an example, if you commit and then realize you forgot to stage the changes in a
file you wanted to add to this commit, you can do something like this:
All three of these commands end up with a single commit — the second command replaces the results of the first.
Unstaginga Staged File
The next two sections demonstrate how to wrangle your staging area and working directory changes. The nice part is that the command you use to determine the state of those two areas also reminds you how to undo changes to them. For example, let’s say you’ve changed two files and want to commit them as two separate changes, but you accidentally type git add * and stage them both. How can you unstage one of the two? The git status command reminds you:
$ git add . $ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: README.txt
# modified: benchmarks.rb #
Right below the "Changes to be committed" text, it says use git reset HEAD <file>... to unstage. So, let’s use that advice to unstage the benchmarks.rb file:
$ git reset HEAD benchmarks.rb benchmarks.rb: locally modified $ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: README.txt
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: benchmarks.rb #
The command is a bit strange, but it works. The benchmarks.rb file is modified but once again unstaged.
Unmodifying a Modified File
What if you realize that you don’t want to keep your changes to the benchmarks.rb file? How can you easily unmodify it — revert it back to what it looked like when you last committed (or initially cloned, or however you got it into your working directory)? Luckily, git status tells you how to do that, too. In the last example output, the unstaged area looks like this:
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: benchmarks.rb #
It tells you pretty explicitly how to discard the changes you’ve made (at least, the newer versions of Git, 1.6.1 and later, do this — if you have an older version, we highly recommend upgrading it to get some of these nicer usability features). Let’s do what it says:
$ git checkout -- benchmarks.rb $ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: README.txt #
You can see that the changes have been reverted. You should also realize that this is
a dangerous command: any changes you made to that file are gone — you just copied another file over it. Don’t ever use this command unless you absolutely know that you don’t want the file. If you just need to get it out of the way, we’ll go over stashing and branching in the next chapter; these are generally better ways to go. Remember,any thing that is committed in Git can almost always be recovered. Even commits that were on branches that were deleted or commits that were overwritten with an --amend commit can be recovered (see Chapter 9 for data recovery). However, anything you lose that was never committed is likely never to be seen again.
WorkingwithRemotes
To be able to collaborate on any Git project, you need to know how to manage your remote repositories. Remote repositories are versions of your project that are hosted on the Internet or network somewhere. You can have several of them, each of which generally is either read-only or read/write for you. Collaborating with others involves managing these remote repositories and pushing and pulling data to and from them when you need to share work. Managing remote repositories includes knowing how to add remote repositories, remove remotes that are no longer valid, manage various remote branches and define them as being tracked or not, and more. In this section, we’ll cover these remote-management skills.
Showing Your Remotes
To see which remote servers you have configured, you can run the git remote command. It lists the shortnames of each remote handle you’ve specified. If you’ve cloned your repository, you should at least see origin — that is the default name Git gives to the server you cloned from:
This means we can pull contributions from any of these users pretty easily. But notice that only the origin remote is an SSH URL, so it’s the only one I can push to (we’ll cover why this is in Chapter 4).
Adding Remote Repositories
I’ve mentioned and given some demonstrations of adding remote repositories in previous sections, but here is how to do it explicitly. To add a new remote Git repository as a shortname you can reference easily, run git remote add [shortname] [url]:
Now you can use the string pb on the command line in lieu of the whole URL. For example, if you want to fetch all the information that Paul has but that you don’t yet have in your repository, you can run git fetch pb:
Paul’s master branch is accessible locally as pb/master — you can merge it into one of your branches, or you can check out a local branch at that point if you want to inspect it.
Fetching and Pulling from Your Remotes
As you just saw, to get data from your remote projects, you can run
$ git fetch [remote-name]
The command goes out to that remote project and pulls down all the data from that remote project that you don’t have yet. After you do this, you should have references to all the branches from that remote, which you can merge in or inspect at any time. (We’ll go over what branches are and how to use them in much more detail in Chapter 3.)
If you cloned a repository, the command automatically adds that remote repository under the name origin. So, git fetch origin fetches any new work that has been pushed to that server since you cloned (or last fetched from) it. It’s important to note that the fetch command pulls the data to your local repository — it doesn’t automatically merge it with any of your work or modify what you’re currently working on. You have to merge it manually into your work when you’re ready.
If you have a branch set up to track a remote branch (see the next section and Chapter 3 for more information), you can use the git pull command to automatically fetch and then merge a remote branch into your current branch. This may be an easier or more comfortable workflow for you; and by default, the git clone command automatically sets up your local master branch to track the remote master branch on the server you cloned from (assuming the remote has a master branch). Running git pull generally fetches data from the server you originally cloned from and automatically tries to merge it into the code you’re currently working on
Pushing to Your Remotes
When you have your project at a point that you want to share, you have to push it upstream. The command for this is simple: git push [remote-name] [branch-name]. If you want to push your master branch to your origin server (again, cloning generally sets up both of those names for you automatically), then you can run this to push your work back up to the server:
$ git push origin master
This command works only if you cloned from a server to which you have write access and if nobody has pushed in the meantime. If you and someone else clone at the same time and they push upstream and then you push upstream, your push will rightly be rejected. You’ll have to pull down their work first and incorporate it into yours before you’ll be allowed to push. See Chapter 3 for more detailed information on how to push to remote servers.
Inspecting a Remote
If you want to see more information about a particular remote, you can use the git remote show [remote-name] command. If you run this command with a particular shortname, such as origin, you get something like this:
$ git remote show origin
* remote origin
URL: git://github.com/wingadium1/ticgit.git
Remote branch merged with 'git pull' while on branch master
master
Tracked remote branches
master
ticgit
It lists the URL for the remote repository as well as the tracking branch information. The command helpfully tells you that if you’re on the master branch and you run git pull, it will automatically merge in the master branch on the remote after it fetches all
the remote references. It also lists all the remote references it has pulled down.
That is a simple example you’re likely to encounter. When you’re using Git more heavily, however, you may see much more information from git remote show:
$ git remote show origin
* remote origin
URL: [email protected]:defunkt/github.git
Remote branch merged with 'git pull' while on branch issues
issues
Remote branch merged with 'git pull' while on branch master
master
New remote branches (next fetch will store in remotes/origin)
caching
Stale tracking branches (use 'git remote prune')
libwalker
walker2
Tracked remote branches
acl
apiv2
dashboard2
issues
master
postgres
Local branch pushed with 'git push'
master:master
This command shows which branch is automatically pushed when you run git push on certain branches. It also shows you which remote branches on the server you don’t yet have, which remote branches you have that have been removed from the server, and multiple branches that are automatically merged when you run git pull.
Removing and Renaming Remotes
If you want to rename a reference, in newer versions of Git you can run git remote rename to change a remote’s shortname. For instance, if you want to rename pb to paul, you can do so with git remote rename:
$ git remote rename pb paul
$ git remote
origin
paul
It’s worth mentioning that this changes your remote branch names, too. What used to be referenced at pb/master is now at paul/master. If you want to remove a reference for some reason — you’ve moved the server or are no longer using a particular mirror, or perhaps a contributor isn’t contributing anymore — you can use git remote rm:
$ git remote rm paul
$ git remote
origin
Tagging
Like most VCSs, Git has the ability to tag specific points in history as being important. Generally, people use this functionality to mark release points (v1.0, and so on). In this you’ll learn how to list the available tags, how to create new tags, and what the different types of tags are.
Listing Your Tags
Listing the available tags in Git is straightforward. Just type git tag:
$ git tag
v0.1
v1.3
This command lists the tags in alphabetical order; the order in which they appear has no real importance. You can also search for tags with a particular pattern. The Git source repo, for instance, contains more than 240 tags. If you’re only interested in looking at the 1.4.2 series, you can run this:
$ git tag -l 'v1.4.2.*'
v1.4.2.1
v1.4.2.2
v1.4.2.3
v1.4.2.4
Creating Tags
Git uses two main types of tags: lightweight and annotated. A lightweight tag is very much like a branch that doesn’t change — it’s just a pointer to a specific commit. Annotated tags, however, are stored as full objects in the Git database. They’re checksummed; contain the tagger name, e-mail, and date; have a tagging message; and can be signed and verified with GNU Privacy Guard (GPG). It’s generally recommended that you create annotated tags so you can have all this information; but if you want a temporary tag or for some reason don’t want to keep the other information, lightweight tags are available too.
Annotated Tags
Creating an annotated tag in Git is simple. The easiest way is to specify -a when you run the tag command:
$ git tag -a v1.4 -m 'my version 1.4'
$ git tag v0.1 v1.3 v1.4
The -m specifies a tagging message, which is stored with the tag. If you don’t specify a message for an annotated tag, Git launches your editor so you can type it in. You can see the tag data along with the commit that was tagged by using the git show command:
$ git show v1.4
tag v1.4
Tagger: Hoang Thanh Son <[email protected]>
Date: Mon Feb 9 14:45:11 2009 -0800
my version 1.4
commit 15027957951b64cf874c3557a0f3547bd83b3ff6
Merge: 4a447f7... a6b4c97...
Author: Hoang Thanh Son <[email protected]>
Date: Sun Feb 8 19:02:46 2009 -0800
Merge branch 'experiment'
That shows the tagger information, the date the commit was tagged, and the annotation message before showing the commit information.
SignedTags
You can also sign your tags with GPG, assuming you have a private key. All you have to do is use -s instead of -a:
$ git tag -s v1.5 -m 'my signed 1.5 tag'
You need a passphrase to unlock the secret key for user: "Hoang Thanh Son <[email protected]>"
1024-bit DSA key, ID F721C45A, created 2009-02-09
If you run git show on that tag, you can see your GPG signature attached to it:
$ git show v1.5
tag v1.5
Tagger: Hoang Thanh Son <[email protected]>
Date: Mon Feb 9 15:22:20 2009 -0800
my signed 1.5 tag
-----BEGIN PGP SIGNATURE----
Version: GnuPG v1.4.8 (Darwin)
iEYEABECAAYFAkmQurIACgkQON3DxfchxFr5cACeIMN+ZxLKggJQf0QYiQBwgySN Ki0An2JeAVUCAiJ7Ox6ZEtK+NvZAj82/=WryJ
-----END PGP SIGNATURE----
commit 15027957951b64cf874c3557a0f3547bd83b3ff6
Merge: 4a447f7... a6b4c97...
Author: Hoang Thanh Son <[email protected]>
Date: Sun Feb 8 19:02:46 2009 -0800
Merge branch 'experiment'
A bit later, you’ll learn how to verify signed tags.
Light weight Tags
Another way to tag commits is with a lightweight tag. This is basically the commit checksum stored in a file — no other information is kept. To create a lightweight tag, don’t supply the -a, -s, or -m option:
$ git tag v1.4-lw
$ git tag
v0.1
v1.3
v1.4
v1.4-lw
v1.5
This time, if you run git show on the tag, you don’t see the extra tag information. The command just shows the commit:
$ git show v1.4-lw
commit 15027957951b64cf874c3557a0f3547bd83b3ff6
Merge: 4a447f7... a6b4c97...
Author: Hoang Thanh Son <[email protected]>
Date: Sun Feb 8 19:02:46 2009 -0800
Merge branch 'experiment'
Verifying Tags
To verify a signed tag, you use git tag -v [tag-name]. This command uses GPG to verify the signature. You need the signer’s public key in your keyring for this to work properly:
$ git tag -v v1.4.2.1
object 883653babd8ee7ea23e6a5c392bb739348b1eb61
type commit
tag v1.4.2.1
tagger Junio C Hamano <[email protected]> 1158138501 -0700
GIT 1.4.2.1
Minor fixes since 1.4.2, including git-mv and git-http with alternates.
gpg: Signature made Wed Sep 13 02:08:25 2006 PDT using DSA key ID F3119B9A
gpg: Good signature from "Junio C Hamano <[email protected]>"
gpg: aka "[jpeg image of size 1513]"
Primary key fingerprint: 3565 2A26 2040 E066 C9A7 4A7D C0C6 D9A4 F311 9B9A
If you don’t have the signer’s public key, you get something like this instead:
gpg: Signature made Wed Sep 13 02:08:25 2006 PDT using DSA key ID F3119B9A
gpg: Can't check signature: public key not found
error: could not verify the tag 'v1.4.2.1'
Tagging Later
You can also tag commits after you’ve moved past them. Suppose your commit history looks like this:
$ git log --pretty=oneline
15027957951b64cf874c3557a0f3547bd83b3ff6 Merge branch 'experiment'
a6b4c97498bd301d84096da251c98a07c7723e65 beginning write support
0d52aaab4479697da7686c15f77a3d64d9165190 one more thing
6d52a271eda8725415634dd79daabbc4d9b6008e Merge branch 'experiment'
0b7434d86859cc7b8c3d5e1dddfed66ff742fcbc added a commit function
4682c3261057305bdd616e23b64b0857d832627b added a todo file
166ae0c4d3f420721acbb115cc33848dfcc2121a started write support
9fceb02d0ae598e95dc970b74767f19372d61af8 updated rakefile
964f16d36dfccde844893cac5b347e7b3d44abbc commit the todo
8a5cbc430f1a9c3d00faaeffd07798508422908a updated readme
Now, suppose you forgot to tag the project at v1.2, which was at the "updated rakefile" commit. You can add it after the fact. To tag that commit, you specify the commit checksum (or part of it) at the end of the command:
$ git tag -a v1.2 9fceb02
You can see that you’ve tagged the commit:
$ git tag
v0.1
v1.2
v1.3
v1.4
v1.4-lw
v1.5
$ git show v1.2
tag v1.2
Tagger: Hoang Thanh Son <[email protected]>
Date: Mon Feb 9 15:32:16 2009 -0800
version 1.2
commit 9fceb02d0ae598e95dc970b74767f19372d61af8
Author: Magnus Chacon <[email protected]>
Date: Sun Apr 27 20:43:35 2008 -0700
updated rakefile
...
Sharing Tags
By default, the git push command doesn’t transfer tags to remote servers. You will have to explicitly push tags to a shared server after you have created them. This process is just like sharing remote branches you can run git push origin [tagname].
If you have a lot of tags that you want to push up at once, you can also use the --tags option to the git push command. This will transfer all of your tags to the remote server that are not already there.
Now, when someone else clones or pulls from your repository, they will get all your tags as well.
Tips and Tricks
Before we finish this chapter on basic Git, a few little tips and tricks may make your Git experience a bit simpler, easier, or more familiar. Many people use Git without using any of these tips, and we won’t refer to them or assume you’ve used them later in the book; but you should probably know how to do them.
Auto-Completion
If you use the Bash shell, Git comes with a nice auto-completion script you can enable. Download the Git source code, and look in the contrib/completion directory; there should be a file called git-completion.bash. Copy this file to your home directory, and add this to your .bashrc file:
source ˜/.git-completion.bash
If you want to set up Git to automatically have Bash shell completion for all users, copy this script to the /opt/local/etc/bash_completion.d directory on Mac systems or to the /etc/bash_completion.d/ directory on Linux systems. This is a directory of scripts that Bash will automatically load to provide shell completions.
If you’re using Windows with Git Bash, which is the default when installing Git on Windows with msysGit, auto-completion should be preconfigured.
Press the Tab key when you’re writing a Git command, and it should return a set of suggestions for you to pick from:
$ git co<tab><tab>
commit config
In this case, typing git co and then pressing the Tab key twice suggests commit and config. Adding m<tab> completes git commit automatically.
This also works with options, which is probably more useful. For instance, if you’re running a git log command and can’t remember one of the options, you can start typing it and press Tab to see what matches:
That’s a pretty nice trick and may save you some time and documentation reading.
Git Aliases
Git doesn’t infer your command if you type it in partially. If you don’t want to type the entire text of each of the Git commands, you can easily set up an alias for each command using git config. Here are a couple of examples you may want to set up:
This means that, for example, instead of typing git commit, you just need to type git ci. As you go on using Git, you’ll probably use other commands frequently as well; in this case, don’t hesitate to create new aliases.
This technique can also be very useful in creating commands that you think should exist. For example, to correct the usability problem you encountered with unstaging a file, you can add your own unstage alias to Git:
$ git config --global alias.unstage 'reset HEAD --'
This makes the following two commands equivalent:
$ git unstage fileA $ git reset HEAD fileA
This seems a bit clearer. It’s also common to add a last command, like this:
$ git config --global alias.last 'log -1 HEAD'
This way, you can see the last commit easily:
$ git
last commit 66938dae3329c7aebe598c2246a8e6af90d04646
Author: Josh Goebel <[email protected]>
Date: Tue Aug 26 19:48:51 2008 +0800
test for current head
Signed-off-by: Hoang Thanh Son <[email protected]>
As you can tell, Git simply replaces the new command with whatever you alias it for. However, maybe you want to run an external command, rather than a Git subcommand. In that case, you start the command with a ! character. This is useful if you write your own tools that work with a Git repository. We can demonstrate by aliasing git visual to run gitk:
$ git config --global alias.visual "!gitk"
Summary
At this point, you can do all the basic local Git operations — creating or cloning a repository, making changes, staging and committing those changes, and viewing the history of all the changes the repository has been through. Next, we’ll cover Git’s killer feature: its branching model.