Thứ Hai, 18 tháng 7, 2016

Sử dụng EXPLAIN để tối ưu câu lệnh MySQL



EXPLAIN là câu lệnh trong mysql giúp bạn biết được những gì xảy ra bên trong một câu lệnh khác. Sử dụng explain một cách thành thục sẽ giúp bạn tránh khỏi các câu query tồi, cũng giống như phát hiện ra các bottleneck của hệ thống như chưa dán index...

Khi bạn muốn thực thi một câu truy vấn (query), MySQL Query Optimizer sẽ cố gắng đưa ra một kế hoạch tối ưu nhất cho việc thực hiện query. Bạn có thể thấy thông tin về kế hoạch đó bằng cách thêm lệnh EXPLAIN vào đầu mỗi query. EXPLAIN là một trong những công cụ quan trọng giúp hiểu và tối ưu truy vấn MySQL, tuy nhiên, điều đáng tiếc là rất nhiều lập trình viên hiếm khi dùng nó. Trong bài viết này, bạn sẽ được học ý nghĩa từng thành phần trong kết quả trả về của EXPLAIN và cách dùng nó để tối ưu thiết kế cơ sở dữ liệu cũng như câu truy vấn.

Tìm hiểu kết quả trả về của lệnh EXPLAIN

Việc sử dụng EXPLAIN hết sức đơn giản, chỉ cần thêm nó vào trước SELECT trong câu truy vấn. Trước tiên hãy cùng tìm hiểu kết quả trả về của một câu truy vấn đơn giản để bạn có thể làm quen với các cột trong bảng kết quả.

EXPLAIN SELECT * FROM categories\G
********************** 1. row **********************
           id: 1
  select_type: SIMPLE
        table: categories
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4
        Extra: 
1 row in set (0.00 sec)
Có rất nhiều thông tin được bao hàm trong 10 cột trên. Đó là:




  • id – Số thứ tự cho mỗi câu SELECT trong truy vấn của bạn (trường hợp bạn sử dụng các truy vấn lồng nhau (nested sub queries).
  • select_type – Loại của câu SELECT. Có thể có các giá trị sau.
    • SIMPLE – Truy vấn là một câu SELECT cơ bản, không có bất cứ truy vấn con (subqueries) hay câu lệnh hợp (UNION) nào.
    • PRIMARY – Truy vấn là câu SELECT ngoài cùng của một lệnh JOIN.
    • DERIVED – Truy vấn là một truy vấn con của truy vấn khác, nằm trong lệnh FROM.
    • SUBQUERY – Truy vấn đầu tiên của một truy vấn con.
    • DEPENDENT SUBQUERY – Truy vấn con, phụ thuộc vào một truy vấn khác bên ngoài nó.
    • UNCACHEABLE SUBQUERY – Truy vấn không thể lưu lại được (có quy định điều kiện cụ thể, thế nào là một truy vấn có thể lưu lại được).
    • UNION – Truy vấn là câu SELECT thứ hai của lệnh UNION.
    • DEPENDENT UNION – Truy vấn thứ hai hoặc các truy vấn tiếp theo của lệnh UNIONphụ thuộc vào một truy vấn bên ngoài.
    • UNION RESULT – Truy vấn là kết quả của lệnh UNION.

    • table – Bảng liên quan đến câu truy vấn.
    • type – Cách MySQL join các bảng lại với nhau. Đây là một trong những trường quan trọng nhất của kết quả trả về, nó chỉ ra đâu là nơi thiếu chỉ mục (index) và làm cách nào truy vấn của bạn cần phải xem xét lại. Các giá trị trả về có thể là.
      • system – Bảng không có hoặc chỉ có 1 dòng.
      • const – Bảng chỉ có duy nhất 1 dòng đã được đánh chỉ mục mà khớp với điều kiện tìm kiếm. Đây là loại join nhanh nhất, bởi bảng chỉ cần đọc một lần duy nhất và giá trị của cột được xem như là hằng số khi join với các bảng khác.
      • eq_ref – Tất cả các thành phần của index được sử dụng bởi lệnh join và index thuộc loại PRIMARY KEY hoặc UNIQUE NOT NULL. Đây là loại join tốt thứ hai (chỉ sau const).
      • ref – Tất cả các dòng khớp với điều kiện tìm kiếm và chưa cột đã được index đều được đọc cho mỗi sự kết hợp với các dòng của bảng trước đó. Loại join này có thể thấy khi so sánh cột với điều kiện = hoặc <=>.
      • fulltext – join sử dụng chỉ mục dạng FULLTEXT.
      • ref_or_null – Gần giống như ref nhưng chứa cả các dòng với cột mang giá trị null.
      • index_merge – join sử dụng một danh sách các chỉ mục để tạo ra tập kết quả. Cộtkey trong kết quả của EXPLAIN sẽ liệt kê các khóa được sử dụng.
      • unique_subquery – Truy vấn con với lệnh IN trả về duy nhất một kết quả và sử dụng primary key.
      • index_subquery – Gần giống như unique_subquery nhưng trả về nhiều hơn một dòng.
      • range – Chỉ mục được dùng để tìm ra các dòng thỏa mãn điều kiện tìm kiếm, cụ thể là khi khóa được so sánh với hằng số thông qua các toán tử BETWEEN, IN, >,>=,…
      • index – Toàn bộ cây chỉ mục được duyệt để tìm ra dòng thỏa mãn điều kiện.
      • all – Toàn bộ bảng được duyệt để tìm dòng cho join. Đây là loại join tồi tệ nhất và thường cho thấy sự thiếu xót trong việc đánh chỉ mục.
    • possible_keys – Hiển thị keys có thể được dùng bởi MySQL để tìm dòng trong bảng, tuy nhiên nó có thể hoặc không thể được dùng. Trong thực tế, cột này đôi khi giúp cho việc tối ưu truy vấn, bởi nếu cột này trống (NULL), nó thường cho thấy không có chỉ mục liên quan được định nghĩa trong bảng.
    • key – Khóa thực sự được sử dụng bởi MySQL. Cột này có thể chứa khóa không được liệt kê ở cột possible_keys. Trình tối ưu của MySQL luôn cố gắng tìm kiếm khóa tối ưu nhất cho truy vấn. Khi kết hợp nhiều bảng, nó có thể dùng khóa không nằm trong danh sách possible_keys nhưng lại đem về hiệu quả cao hơn.
    • key_len – Chiều dài của khóa mà trình tối ưu truy vấn (Query Optimizer) sử dụng. Ví dụ,key_len mang giá trị 4 có nghĩa là nó cần bộ nhớ để lưu 4 ký tự. Bạn đọc có thể xem lạiYêu cầu về bộ nhớ cho từng kiểu dữ liệu trong MySQL.
    • ref – Tên cột hoặc hằng số được dùng để so sánh với chỉ mục được nêu ra ở cột key. MySQL có thể lấy ra một hằng số, hoặc một cột cho quá trình thực hiện truy vấn. Bạn có thể thấy trong ví dự sẽ được liệt kê dưới đây.
    • rows – Số lượng bản ghi đã được duyệt để trả về kết quả. Đây cũng là một cột hết sức quan trọng cho việc tối ưu truy vấn, nhất là khi bạn dùng JOIN hoặc truy vấn con.
    • Extra – Các thông tin bổ sung liên quan đến quá trình thực hiện truy vấn. Các giá trị kiểu như Using Temporary (dùng tạm thời), Using filesort (dùng sắp xếp file),… của cột này có thể cho thấy một truy vấn không thực sự tốt. Danh sách đầy đủ của các giá trị có thể có ở cột này có thể xem tại Tài liệu MySQL
    Bạn có thể bổ sung thêm từ khóa EXTENDED sau EXPLAIN và MySQL sẽ đưa ra các thông tin bổ sung về quá trình thực hiện truy vấn. Để xem chi tiết, thực hiện lệnh SHOW WARNINGS ngay sau lệnh EXPLAIN. Nó thường được dùng để xem các câu truy vấn được thực hiện sau bất cứ thay đổi nào được tạo ra bởi Query Optimizer.

    EXPLAIN EXTENDED SELECT City.Name FROM City
    JOIN Country ON (City.CountryCode = Country.Code)
    WHERE City.CountryCode = 'IND' AND Country.Continent = 'Asia'\G
    ********************** 1. row **********************
               id: 1
      select_type: SIMPLE
            table: Country
             type: const
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 3
              ref: const
             rows: 1
         filtered: 100.00
            Extra: 
    ********************** 2. row **********************
               id: 1
      select_type: SIMPLE
            table: City
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 4079
         filtered: 100.00
            Extra: Using where
    2 rows in set, 1 warning (0.00 sec)
    SHOW WARNINGS\G
    ********************** 1. row **********************
      Level: Note
       Code: 1003
    Message: select `World`.`City`.`Name` AS `Name` from `World`.`City` join `World`.`Country` where ((`World`.`City`.`CountryCode` = 'IND'))
    1 row in set (0.00 sec)

    Khắc phục sự cố về hiệu năng với EXPLAIN

    Giờ hãy cùng tìm hiểu làm cách nào chúng ta có thể tối ưu một truy vấn hiệu năng thấp bằng cách phân tích kết quả của EXPLAIN. Trong thực tế, không có gì phải nghi ngờ khi chúng ta sẽ có rất nhiều bảng với rất nhiều quan hệ với nhau, tuy nhiên đôi khi thật khó để biết được cách tốt nhất để viết một truy vấn.
    Ở đây, tôi đã tạo ra một cơ sở dữ liệu mẫu cho một ứng dụng thương mại mà ở đó không có chỉ mục, khóa chính và sẽ mô tả ảnh hưởng của thiết kế tồi tệ này bằng cách viết ra các truy vấn phức tạp. Bạn có thể download Mô hình DB từ Github.



    EXPLAIN SELECT * FROM
    orderdetails d
    INNER JOIN orders o ON d.orderNumber = o.orderNumber
    INNER JOIN products p ON p.productCode = d.productCode
    INNER JOIN productlines l ON p.productLine = l.productLine
    INNER JOIN customers c on c.customerNumber = o.customerNumber
    WHERE o.orderNumber = 10101\G
    ********************** 1. row **********************
               id: 1
      select_type: SIMPLE
            table: l
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 7
            Extra: 
    ********************** 2. row **********************
               id: 1
      select_type: SIMPLE
            table: p
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 110
            Extra: Using where; Using join buffer
    ********************** 3. row **********************
               id: 1
      select_type: SIMPLE
            table: c
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 122
            Extra: Using join buffer
    ********************** 4. row **********************
               id: 1
      select_type: SIMPLE
            table: o
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 326
            Extra: Using where; Using join buffer
    ********************** 5. row **********************
               id: 1
      select_type: SIMPLE
            table: d
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 2996
            Extra: Using where; Using join buffer
    5 rows in set (0.00 sec)
    Nhìn vào kết quả trên, bạn thấy được tất cả biểu hiện của một truy vấn tồi tệ. Tuy nhiên, kể cả khi tôi có viết truy vấn tốt hơn, thì kết quả vẫn tương tự, bởi không có chỉ mục. Loại join làALL (loại tồi nhất), có nghĩa rằng MySQL không thể xác định bất cứ khóa nào để dùng cho join, và do vậy cả possible_keyskey đều trống. Quan trọng hơn, trường rows cho thấy MySQL phải duyệt tất cả các bản ghi của từng bảng cho câu truy vấn này. Có nghĩa, để chạy câu truy vấn, nó cần duyệt 7*110*122*326*2996 = 91,750,822,240 bản ghi để tìm ra kết quả. Điều này thật khủng khiếp và nó sẽ còn tăng thêm khi cơ sở dữ liệu lớn hơn.
    Bây giờ, chúng ta thử thêm 1 số chỉ mục khá hiển nhiên, như khóa chính cho từng bảng, và thực hiện truy vấn một lần nữa. Theo thông lệ cơ bản, bạn tìm đến các cột dùng để JOIN và cho chúng làm khóa, bởi MySQL sẽ luôn tìm theo các cột đó để tra cứu các bản ghi.

    ALTER TABLE customers
        ADD PRIMARY KEY (customerNumber);
    ALTER TABLE employees
        ADD PRIMARY KEY (employeeNumber);
    ALTER TABLE offices
        ADD PRIMARY KEY (officeCode);
    ALTER TABLE orderdetails
        ADD PRIMARY KEY (orderNumber, productCode);
    ALTER TABLE orders
        ADD PRIMARY KEY (orderNumber),
        ADD KEY (customerNumber);
    ALTER TABLE payments
        ADD PRIMARY KEY (customerNumber, checkNumber);
    ALTER TABLE productlines
        ADD PRIMARY KEY (productLine);
    ALTER TABLE products 
        ADD PRIMARY KEY (productCode),
        ADD KEY (buyPrice),
        ADD KEY (productLine);
    ALTER TABLE productvariants 
        ADD PRIMARY KEY (variantId),
        ADD KEY (buyPrice),
        ADD KEY (productCode);
    Giờ chúng ta chạy lại truy vấn sau khi đã thêm chỉ mục.

    ********************** 1. row **********************
               id: 1
      select_type: SIMPLE
            table: o
             type: const
    possible_keys: PRIMARY,customerNumber
              key: PRIMARY
          key_len: 4
              ref: const
             rows: 1
            Extra: 
    ********************** 2. row **********************
               id: 1
      select_type: SIMPLE
            table: c
             type: const
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 4
              ref: const
             rows: 1
            Extra: 
    ********************** 3. row **********************
               id: 1
      select_type: SIMPLE
            table: d
             type: ref
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 4
              ref: const
             rows: 4
            Extra: 
    ********************** 4. row **********************
               id: 1
      select_type: SIMPLE
            table: p
             type: eq_ref
    possible_keys: PRIMARY,productLine
              key: PRIMARY
          key_len: 17
              ref: classicmodels.d.productCode
             rows: 1
            Extra:
    ********************** 5. row **********************
               id: 1
      select_type: SIMPLE
            table: l
             type: eq_ref
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 52
              ref: classicmodels.p.productLine
             rows: 1
            Extra: 
    5 rows in set (0.00 sec)
    Sau khi thêm chỉ mục, số lượng bản ghi cần duyệt giảm xuống còn 1*1*4*1*1 = 4. Điều đó có nghĩa là, với mỗi bản ghi có orderNumber là 10101 trong bảng orderDetails, MySQL có thể tìm trự tiếp bản ghi thỏa mãn trong tất cả các bản ghi khác bằng cách sử dụng chỉ mục và không cần phải duyệt lại cả bảng.
    Ở dòng đầu tiên, loại join là const, loại nhanh nhất với bảng có nhiều hơn 1 bản ghi. MySQL có thể sử dụng khóa chính trong trường hợp này. Trường ref trả về kết quả const, có nghĩa là không gì ngoài giá trị 10101 được dùng trong lệnh WHERE.

    Tiếp theo hãy xem một truy vấn khác. Ở đây, chúng ta chỉ đơn giản hợp 2 bảng lại, productsproductvariants, cả hai đều được join với productline. Bảng productvariants gồm các biến thể của productCode ở dạng khóa ngoài.

    EXPLAIN SELECT * FROM (
    SELECT p.productName, p.productCode, p.buyPrice, l.productLine, p.status, l.status AS lineStatus FROM
    products p
    INNER JOIN productlines l ON p.productLine = l.productLine
    UNION
    SELECT v.variantName AS productName, v.productCode, p.buyPrice, l.productLine, p.status, l.status AS lineStatus FROM productvariants v
    INNER JOIN products p ON p.productCode = v.productCode
    INNER JOIN productlines l ON p.productLine = l.productLine
    ) products
    WHERE status = 'Active' AND lineStatus = 'Active' AND buyPrice BETWEEN 30 AND 50G
    ********************** 1. row **********************
               id: 1
      select_type: PRIMARY
            table: <derived2>
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 219
            Extra: Using where
    ********************** 2. row **********************
               id: 2
      select_type: DERIVED
            table: p
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 110
            Extra: 
    ********************** 3. row **********************
               id: 2
      select_type: DERIVED
            table: l
             type: eq_ref
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 52
              ref: classicmodels.p.productLine
             rows: 1
            Extra: 
    ********************** 4. row **********************
               id: 3
      select_type: UNION
            table: v
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 109
            Extra:    
    ********************** 5. row **********************
               id: 3
      select_type: UNION
            table: p
             type: eq_ref
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 17
              ref: classicmodels.v.productCode
             rows: 1
            Extra:  
    ********************** 6. row **********************
               id: 3
      select_type: UNION
            table: l
             type: eq_ref
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 52
              ref: classicmodels.p.productLine
             rows: 1
            Extra:
    ********************** 7. row **********************
               id: NULL
      select_type: UNION RESULT
            table: <union2,3>
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: NULL
            Extra: 
    7 rows in set (0.01 sec)
    Bạn có thể thấy rất nhiều vấn đề ở câu truy vấn này. Nó duyệt tất cả bản ghi ở bảngproductsproductvariants. Vì không có chỉ mục nào ở các bảng này cho trườngproductLinebuyPrice, kết quả trả về cột possible_keyskey đều là trống. Trạng thái của productsproductlines được kiểm tra sau khi hợp lại UNION, do vậy việc cho chúng vào trong UNION sẽ giảm số lượng bản ghi. Giờ chúng ta thử thêm vài chỉ mục và viết lại truy vấn.

    CREATE INDEX idx_buyPrice ON products(buyPrice);
    CREATE INDEX idx_buyPrice ON productvariants(buyPrice);
    CREATE INDEX idx_productCode ON productvariants(productCode);
    CREATE INDEX idx_productLine ON products(productLine);
    EXPLAIN SELECT * FROM (
    SELECT p.productName, p.productCode, p.buyPrice, l.productLine, p.status, l.status as lineStatus FROM products p
    INNER JOIN productlines AS l ON (p.productLine = l.productLine AND p.status = 'Active' AND l.status = 'Active') 
    WHERE buyPrice BETWEEN 30 AND 50
    UNION
    SELECT v.variantName AS productName, v.productCode, p.buyPrice, l.productLine, p.status, l.status FROM productvariants v
    INNER JOIN products p ON (p.productCode = v.productCode AND p.status = 'Active') 
    INNER JOIN productlines l ON (p.productLine = l.productLine AND l.status = 'Active')
    WHERE
    v.buyPrice BETWEEN 30 AND 50
    ) productG
    ********************** 1. row **********************
              id: 1
      select_type: PRIMARY
            table: <derived2>
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 12
            Extra: 
    ********************** 2. row **********************
               id: 2
      select_type: DERIVED
            table: p
             type: range
    possible_keys: idx_buyPrice,idx_productLine
              key: idx_buyPrice
          key_len: 8
              ref: NULL
             rows: 23
            Extra: Using where
    ********************** 3. row **********************
               id: 2
      select_type: DERIVED
            table: l
             type: eq_ref
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 52
              ref: classicmodels.p.productLine
             rows: 1
            Extra: Using where
    ********************** 4. row **********************
               id: 3
      select_type: UNION
            table: v
             type: range
    possible_keys: idx_buyPrice,idx_productCode
              key: idx_buyPrice
          key_len: 9
              ref: NULL
             rows: 1
            Extra: Using where
    ********************** 5. row **********************
               id: 3
      select_type: UNION
            table: p
             type: eq_ref
    possible_keys: PRIMARY,idx_productLine
              key: PRIMARY
          key_len: 17
              ref: classicmodels.v.productCode
             rows: 1
            Extra: Using where
    ********************** 6. row **********************
               id: 3
      select_type: UNION
            table: l
             type: eq_ref
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 52
              ref: classicmodels.p.productLine
             rows: 1
            Extra: Using where
    ********************** 7. row **********************
               id: NULL
      select_type: UNION RESULT
            table: <union2,3>
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: NULL
            Extra: 
    7 rows in set (0.01 sec)
    Như bạn thấy ở kết quả, số lượng dòng được duyệt đả giảm đáng kể, từ 2,625,810 (219 * 110 * 109) xuống còn 276 (12 * 23), thực sự là một hiệu quả rất đáng ghi nhận. Nếu bạn cố gắng chạy cùng truy vấn, mà không sắp xếp lại từ trước, ngay sau khi thêm chỉ mục, bạn sẽ không thể thấy được số lượng giảm đáng kể như vậy. MySQL không thể sử dụng chỉ mục bởi nó dùng WHERE trong kết quả trả về. Sau khi di chuyển điều kiện vào bên trong UNION, MySQL đã có thể dùng chỉ mục. Điều đó có nghĩa là thêm chỉ mục không phải là đủ, MySQL sẽ không thể dùng được chỉ mục đó trừ khi bạn viết câu truy vấn thích hợp.

    Kết luận

    Trong bài viết này, tôi đã thảo luận về từ khóa EXPLAIN của MySQL, ý nghĩa của kết quả trả về và cách dùng nó để viết truy vấn tốt hơn. Trong thực tế, nó còn có thể hữu dụng hơn ví dụ được đưa ra ở đây. Thực tế, bạn sẽ thường xuyên join nhiều bảng với câu lệnh WHERE phức tạp. Việc chỉ đơn giản thêm chỉ mục sẽ không phải lúc nào cũng giúp bạn, bạn cần dành thời gian suy nghĩ kỹ càng hơn và viết truy vấn tốt hơn.

    Quan điểm cá nhân

    Lần đầu tiên tôi biết đến EXPLAIN cũng là khi tôi biết được chương trình của mình thực hiện quá nhiều truy vấn sử dụng filesort (Phần Extra trả về Using filesort). Đây là 1 sắp xếp không có index và rất nguy hiểm khi làm việc với lượng dữ liệu lớn. Việc dùng EXPLAIN giúp tôi phát hiện ra thiếu sót trong thiết kế DB của mình ở thời điểm đó và qua đó biết cách chỉnh sửa thiết kế của mình.

    Ngày nay, việc sử dụng các framework hỗ trợ ORM khiến cho các lập trình viên không quan tâm đến quá trình truy vấn dữ liệu trong DB, kéo theo đó là các vấn đề về hiệu năng khi ứng dụng to lên. Ngược lại, việc viết truy vấn bằng tay, dù tốn thời gian nhưng lại đem lại hiệu quả cao về hiệu năng sau này cũng như nâng cao hiểu biết của lập trình viên. Khi tôi còn là thực tập sinh, tôi phải viết tất cả truy vấn bằng tay và theo chỉ định của người hướng dẫn, tôi phải tự EXPLAIN tất cả query trước khi code. Những trải nghiệm đó đã giúp tôi tích lũy được rất nhiều kinh nghiệm cho công việc sau này. Rất mong mọi người cũng có cùng quan điểm và dành thời gian EXPLAIN câu query của mình trước khi bắt tay vào code ứng dụng thực tế.

    Thứ Hai, 11 tháng 4, 2016

    Thiết kế RESTful APIs


    Tại sao API lại quan trọng trong mỗi sản phẩm? Để hiểu đơn giản, bạn có thể tham khảo bài viết này, dù không mới nhưng có một tiêu đề và nội dung rất hay. “Trong thế giới kết nối như bây giờ, một sản phẩm không thể đứng độc lập, và sản phẩm nào không có APIs, giống như máy tính không được kết nối Internet vậy”.



    RESTful

    Tôi không phân tích tại sao lại là RESTful, vì chủ đề này có lẽ cần một bài viết khác, nhưng nếu có cũng thành thừa vì gần như chúng ta đều đồng ý với nhau rằng APIs trong thời đại ngày nay sử dụng tiêu chuẩn RESTful.
    Nhưng nếu chúng ta đã quyết định sử dụng RESTful cho APIs, thì nên hiểu đúng và làm đúng. RESTful có thể tóm lược như sau:
    • URI như tên gọi (Uniform Resource Identifier), chỉ định địa chỉ của mỗi tài nguyên (resource). VD: api.shop.me/customer hoặc api.shop.me/order
    • Các method của HTTP được sử dụng tương ứng với những hành động CRUD trên resource tương ứng. Thông thường: Create – POST, Read – GET, Update – PUT / PATCH, Delete – DELETE
    Hầu hết những webservice cũ chỉ sử dụng 2 phương thức GET và POST và để đơn giản với người sử dụng; nhưng lại sử dụng URI cho cả entity lẫn function. Đừng mang tư tưởng đó khi thiết kế RESTful.

    Tại sao là entity?

    Webservice theo chuẩn cũ coi resource gồm business entity và method / function nhưng thực sự thì method / function không nên là resource, chỉ business entity là resource như RESTful định nghĩa mà thôi. Nhưng tại sao lại “quy hoạch” theo business entity? Vì dễ hơn. Mọi lập trình viên đều dễ làm việc với entity hơn. Chẳng phải ai cũng bắt đầu với một nghiệp vụ với CRUD một entity đó sao? Trong một hệ thống thông thường, các entity có số lượng ít và “đơn giản”, dễ hiểu hơn các nghiệp vụ (method / function). Cung cấp danh sách các entity cùng ít hoạt động CRUD nhằm ẩn đi những nghiệp vụ phức tạp (thường là lời gọi tới hàng chục method), giúp các lập trình viên sử dụng đơn giản hơn.
    Ví dụ, để cập nhật một đơn hàng, chúng ta chỉ đơn giản gọi tới API http://api.shop.me/orders/10 với method PUT kèm theo những thông tin mới, thay vì phải đắn đo giữa một loạt những API như http://api.shop.me/updateOrder, http://api.shop.me/replaceOrder… với hàng loạt những ràng buộc về nghiệp vụ.

    JSON only?

    Hầu hết những nhóm viết RESTful API giờ đây đều chọn JSON là format chính thức nhưng rất nhiều nhóm vẫn phân vân với câu hỏi “chỉ JSON hay hỗ trợ thêm XML?”. Tất nhiên, có hàng tá lý do để chúng ta hỗ trợ thêm những format khác, đặc biệt là XML. Nhưng theo tôi, chỉ cần JSON là đủ. Tôi không tin rằng ngày nay có lập trình viên hay nền tảng nào không biết hoặc không hỗ trợ JSON. Hỗ trợ nhiều định dạng chỉ làm cho việc kiểm thử API thêm phức tạp.

    gzip và binary format?

    Có chăng chúng ta nên nghĩ tới việc sử dụng những format dạng binary để tiết kiệm dữ liệu khi truyền tải qua mạng như BSON, MessagePack… Tuy vậy, hãy để chúng là optional, và người dùng có thể định nghĩa format nhận thông qua HTTP Header.

    Tương tự là câu hỏi “formatted hay compressed JSON? Có nên enable gzip?” (formatted (hay pretty) JSON là JSON được thể hiện ở định dạng “đẹp”, được format bởi tab…; ngược lại compressed JSON loại bỏ hết các dấu tab, space, enter.. nhằm giảm dung lượng). Compressed JSON và gzip rất tuyệt vời vì có thểm giảm từ 30-70% dung lượng truyền tải. Đồng nghĩa với server sẽ tốn tài nguyên và thời gian để xử lý việc nén. Và với tốc độ mạng ngày càng được cải thiện như hiện nay, có lẽ đặt formatted JSON là mặc định thì tốt hơn. Tuỳ vào điều kiện network (VD trên mobile…), client chỉ định định dạng muốn nhận về thông qua HTTP Header (Content-Type hoặc Accept).

    snake_case hay camelCase?

    Well, câu trả lời muôn thủa, kiểu như “thế giới có 2 loại người…”. Việc sử dụng snake case hay camel case chủ yếu do sở thích của lập trình viên thôi, không có lý do gì để phân định được. Camel case tiết kiệm hơn snake case (1 ký tự, orderName so với order_name) là lý do không đủ thuyết phục.
    // snake_case
    {
        customer_id: 100,
        full_name: "Paul Scholsy"
    }
    
    // camelCase
    {
        customerId: 100,
        fullName: "Paul Scholsy"
    }
    Tôi thích snake_case. Vì có vẻ snake_case dễ đọc hơn.

    dash hay underscore (- hay_)?

    Hmm, tương tự như trên. Theo bạn thì http://api.shop.me/orders/last-updated hay http://api.shop.me/orders/last_updated sẽ tốt hơn? Tất nhiên, http://api.shop.me/orders/lastUpdated thì quá tệ, đừng đưa nó vào danh sách lựa chọn của bạn.

    Số ít hay số nhiều?

    Tương tự như trên. Để liệt kê các đơn đặt hàng, chúng ta sẽ đặt URI là http://api.shop.me/order hay http://api.shop.me/orders?
    Chuyện này giống như chúng ta cãi nhau về cách đặt tên bảng trong CSDL quan hệ vậy, Order hay Orders? Best practice cho việc đặt tên bảng trong CSDL quan hệ là sử dụng số ít: Order; nhưng trong api là số nhiều: /orders. Lý do là nó tường minh và gợi nhớ hơn cho người dùng rằng đây là “danh sách đơn hàng”. Rõ ràng, nó tường minh hơn /order (danh sách đơn hàng) và /profile (thông tin người dùng); cùng là dạng số ít nhưng cấu trúc dữ liệu trả về lại khác nhau.
    Câu chuyện phức tạp hơn một chút với /persons hay /people :). Hãy sử dụng /persons.

    Quan hệ

    Đây là vấn đề đau đầu nhất. Bài toán thế này: Một đơn hàng thuộc về một khách hàng và có nhiều sản phẩm. Vậy thông tin đơn hàng trả về sẽ có những gì? Nếu bạn sử dụng CSDL quan hệ thì chắc chắn là bạn đang lưu những thông tin này trong 3 bảng: Order, CustomerProduct. Trả về một entity có những thông tin giống hệt như bảng Order là điều ngu ngốc. Trả về một entity có  những thông tin được JOIN từ 3 bảng trên còn ngu ngốc hơn. Tại sao? Bởi vì nếu thể hiện đúng data model thì chúng ta cần API để làm gì? Thử tưởng tượng dạng đơn giản nhất của entity Order thế này:
    {
        customer_id: 10,
        product_id: [100, 101, 102]
    }
    Và để hiển thị thông tin đơn hàng có tên và địa chỉ khách hàng, tên sản phẩm, client (APIs consumer) cần gọi thêm hàng chục API nữa. Bạn cũng hiểu tại sao không nên trả về thông tin đơn hàng được JOIN từ 3 bảng: có hàng trăm thông tin không cần thiết. Việc cân bằng, tìm ra những thông tin nào nên được trả về trong tình huống này không thực sự đơn giản. Ý tưởng ở đây là API nên trả về thông tin “thiết yếu, liên quan trực tiếp tới nghiệp vụ”. Ví dụ trong tình huống này là:
    http://api.shop.me/orders/10
    {
        customer: {
            id: 105,
            full_name: "Cristiano Messi"
        }
        products: [
            {
                id: 101,
                name: "iPhone"
            },
            {
                id: 102,
                name: "iPad"
            }
        ]
    }
    Nếu client cần thêm thông tin về khách hàng, gọi tới API http://api.shop.me/customers/105
    Nếu client cần thêm thông tin về sản phẩm cho order này, gọi tới API http://api.shop.me/orders/10/products
    {
        meta: {
            page: 1,
            total: 2,
        }
        products: [
            {
                id: 101,
                name: "iPhone"
                sku: "42432",
            },
            {
                id: 102,
                name: "iPad",
                sku: "23112",
            }
        ]
    }
    Và theo trí tưởng tượng phong phú, bạn có thể nghĩ tới việc để client gọi tới API http://api.shop.me/orders/10/products/101 để lấy thông tin chi tiết của sản phẩm có mã 101. Câu trả lời là không. Nếu cần thông tin đầy đủ, client hãy gọi tới API http://api.shop.me/products/101.
    Ở đây tôi chỉ dám đưa ra ý tưởng và ví dụ cơ bản, bởi “thông tin bổ sung” trong quan hệ của những entity là vấn đề thực sự không đơn giản và phụ thuộc rất nhiều vào từng nghiệp vụ cụ thể.

    Pagination

    Hãy luôn sử dụng pagination. Trả về đầy đủ danh sách khách hàng qua API http://api.shop.me/customers là việc tốn kém tài nguyên, đồng thời không hữu dụng. Bởi client cũng sẽ giới hạn lại danh sách này nhằm đáp ứng một giao diện dễ nhìn cho người dùng.
    Hãy để thêm những param cố định trong mỗi API trả về một danh sách dữ liệu như /customers, /orders: page, page_size… để client có thể chỉ định http://api.shop.me/orders?page=4&page_size=10. Luôn sử dụng page_size mặc định để giới hạn dữ liệu trả về ngay cả khi người dùng không chỉ định rõ trong lời gọi API.
    Tất nhiên, pagination thì có hàng chục cách, trên đây chỉ là một ví dụ.

    Filter và dynamic field, dynamic query

    Có một cách để giải quyết vấn đề trả về những dữ liệu quan hệ như trên là sử dụng dynamic field. Ví dụ http://api.shop.me/orders?fields=id,time,customer.fullname,product.id
    Tương tự với dynamic query để thực hiện filter với những điều kiện truy vấn xác định. Trước đây, chúng tôi sử dụng OData và nó khá hiệu quả.
    Ngoài ra, còn 1 khái niệm nữa, là embed: gắn thêm những thông tin liên quan. Ví dụ:
    http://api.shop.me/order/10
    {
        customer_id: 105,
        product_id: [101, 102]
    }
    http://api.shop.me/orders/10?embed=customer

    {
        customer: {
            id: 105,
            full_name: "Cristiano Messi"
        }
        product_id: [101, 102]
    }
    
    Nhưng embed là một vấn đề khó, đừng chú tâm vào nó nếu không cần thiết.

    Metadata

    Luôn trả về metadata với dữ liệu dạng danh sách, dựa vào đó client cũng biết được các truy xuất vào những thông tin khác. Ví dụ: http://api.shop.me/orders
    {
        meta: {
            page: 1,
            page_size: 10,
            total: 2,
        }
        product: [
            {
                id: 101,
            },
            {
                id: 102,
            }
        ]
    }

    Partial update?

    Một đơn hàng (entity Order) chứa 10 field, điều gì xảy ra nếu chúng ta chỉ muốn chỉnh sửa 2 field? Có 2 cách thông thường:
    1. Giữ nguyên dữ liệu được trả về từ API gọi qua GET, với 2 field được chỉnh sửa và gọi API qua PUT
    2. Set  dữ liệu đúng cho 2 field này, set các field còn lại thành null (hoặc zero, empty… – dấu hiệu nhận biết là field này không thay đổi) và gọi API qua PUT
    Cả 2 cách này đều cần gửi lên đầy đủ format của entity. Một cách khác, chỉ cần gửi lên 1 object với đúng 2 field trên, gọi là partial update, thường sử dụng method PATCH.
    Partial update rất hữu dụng, nhưng thường có 2 vấn đề:
    1. Việc deserialise ở phía server để ra đúng object không quá đơn giản
    2. Đôi khi gây bối rối cho người sử dụng API, đặc biệt với các required field.
    Do đó, partial update nên được coi là phần advanced của API và nên xuất hiện ở version 2, 3.. Trước đây chúng tôi sử dụng OData khá tốt (nhưng không thực sự hoàn hảo) cho partial update. Quan tâm tới vấn đề này ngay từ ngày đầu thiết kế API khiến chúng tôi mất khá nhiều thời gian.

    Status code

    Luôn sử dụng HTTP code cho catched exception. Thay vì trả về message “Unauthorised” với HTTP code = 200, hãy trả về 401. Bạn có thể tìm hiểu thêm ở đây: http://www.restapitutorial.com/httpstatuscodes.html
    Nói chung, RESTful APIs chỉ nên dùng những HTTP code 4xx cho những catched exception (unauthorised, validation…). Nếu những lỗi này là không đủ, hãy mở rộng chúng và đặt tham chiếu trong tài liệu API. Ví dụ, lỗi validation thường sử dụng HTTP code là 400 (bad request) hoặc 409 (conflict), cho lời gọi PUT http://api.shop.me/orders/10 trả về HTTP code = 409 cùng message (10xx dùng để validate order, tương tự 11xx cho customer, 24xx cho product…)
    {
        message: "Cannot save order status",
        errors: [
            {
                code: 4091023,
                field: state,
                description: "Order state is incorrect. Please use OPEN, DELIVERING or DONE"
            }
            {
                code: 4091086,
                field: customer_id,
                description: "Customer isn't existing"
            }
        ]
    }

    Authorization

    Bởi RESTful là stateless HTTP nên đừng làm nóng server và client bởi session hay cookies. Cách đúng đắn nhất là sử dụng Authorization Header. OAuth 2 là chuẩn được khuyến nghị cho RESTful APIs.

    Versioning

    Vấn đề này với RESTful APIs phải nói là “hay dữ dội”. Sự phức tạp này đến từ việc khác nhau giữa client-side APIs và server-side APIs: khi sử dụng thư viện trên client, chúng ta hoàn toàn chủ động trong việc chọn version “gắn chặt” nó với chương trình, nhưng gọi qua RESTful thì không phải như vậy. Khi một version mới của RESTful API ra đời, version cũ sẽ sớm “chết”.
    Có 3 cách thường dùng để versioning RESTful API: 2 trong số đó là versioning qua URI: qua sub-domain (http://v1.api.shop.me/orders) hoặc resource path (http://api.shop.me/orders/v1). Cả 2 cách đều dở như nhau vì nó khiến phía client bị hard-code. Cách versioning qua resource path tệ hơn, vì /v1 vốn không phải là 1 property của /orders. Còn nếu bạn dùng http://api.shop.me/v1/orders thì hãy dùng sub-domain, nó còn tuyệt hơn.
    Cách thứ 3 là versioning thông qua HTTP Header (VD: API-Version=v1.1) sẽ tốt hơn nhưng ít người sử dụng, chỉ bởi việc phải đặt thêm HTTP header khiến người dùng không dễ copy-paste URI vào web browser để test.
    Một cách tiếp cận khác là “không sử dụng versioning”, nghĩa là cố gắng hỗ trợ việc tương thích ngược hoặc xác định rõ thời điểm cung cấp API mới và yêu cầu người dùng phải migrate lên version mới trong một khoảng thời gian xác định. Đây là cách Google, Facebook.. đang làm. Nhưng phải nói thật là, chỉ khi nào “chúng ta lớn” hoặc có sản phẩm cùng RESTful APIs là trung tâm cuộc chơi về kết nối, tích hợp thì mới dễ thực hiện theo phương pháp này.
    Tuy nhiên cũng chẳng có vấn đề gì nếu bạn bỏ qua việc versioning từ phiên bản APIs đầu tiên, hoặc versioning thông qua HTTP Header (nếu API-Version không được chỉ định, sử dụng version mới nhất). Nếu chúng ta không làm tốt version đầu tiên thì chắc gì đã cần tới v2, v3… thậm chí là v1.1 

    HATEOAS

    HATEOAS là một trong những chuẩn được khuyến nghị cho RESTful APIs và bạn nên tận dụng. Ví dụ một bài toán: Khi lấy thông tin chi tiết của 1 đơn hàng (entity Order), người dùng hiện tại (nhận biết qua Access Token) chỉ được xem và cập nhật, không được xoá; làm sao client nhận biết được những action này? Cách đơn giản là sử dụng HATEOAS, gắn những hyperlink trong giá trị trả về tương ứng với những hành động có thể thực hiện trên entity.
    http://api.shop.me/orders/10
    {
        customer_id: 10,
        product_id: [100, 101, 102],
        links: [
            {
                rel: "self",
                href: "http://api.shop.me/orders/10",
                method: "GET"
            },
            {
                rel: "order.update",
                href: "http://api.shop.me/orders/10",
                method: "PATCH"
            },
            {
                rel: "order.update",
                href: "http://api.shop.me/orders/10",
                method: "PUT"
            } 
        ]
    }
    
    HATEOAS rất hữu dụng nhưng như bạn thấy, bandwidth dành cho nó cũng không dễ chịu chút nào; tương tự với việc xử lý để tính toán ra những hành động, hyperlink tương ứng. Nếu không cần thiết, hãy hỗ trợ HATEOAS là optional thông qua header.

    Overriding

    Đừng ngần ngại overriding nếu cần thiết. HATEOAS không được liệt kê trong default HTTP header, hãy tự định nghĩa. Tương tự với HTTP method nếu thấy cần thiết. Nhưng hãy nhớ là cần thiết, đừng override Authorization thành shop.me-Authorization nếu bạn không biết mình đang làm gì.

    Tài liệu

    RESTful APIs phải có khả năng self-described hay self-documented; nghĩa là người dùng nhìn vào là biết cách dùng ngay. Tuy vậy, vẫn có những thứ bạn cần viết ra thành tài liệu như: authorization, HTTP header, filter, pagination… và nếu gói gọn trong Getting Started là tốt nhất. Đẹp nhất là người dùng có thể gọi thành công 1 API trong dưới 15 phút thông qua những công cụ đơn giản như curl, Fiddler, REST Console… ngay khi đọc xong Getting Started.
    Có những ngôn ngữ / công cụ rất hay như Swagger để viết APIs document, thậm chí là generate document từ code và cho phép người dùng hand-on với APIs. Nhưng hãy cẩn thận, có lẽ bạn cần sandbox environment để tránh người dùng quá “hứng thú” trong việc khám phá APIs của mình.

    Cuối cùng

    Trên đây tôi chỉ điểm qua một số best practices bạn nên lưu ý khi thiết kế RESTful APIs. Bạn có thể đọc thêm cuốn sách này, không quá hay đâu nhưng có nhiều góc nhìn hơn.
    Tuy vậy, trên tất cả, hãy nhớ rằng “Với một lập trình viên backend, thiết kế APIs chính là thiết kế giao diện. Đừng đi ngược xu thế UX, hãy thiết kế chúng thật đơn giản, hữu dụng và mạnh mẽ”. Những lập trình viên có thể kiên trì hơn người dùng cuối, nhưng bạn cũng chỉ có không quá 30 phút để cho họ hiểu cách sử dụng APIs của mình.

    Thứ Hai, 29 tháng 2, 2016

    Tôi đã dùng Docker như thế nào?

    Ngày nay, các công cụ như Docker hay Vagrant rất hay được nhắc đến trong giới công nghệ, đặc biệt, đây là các công cụ cần thiết cho mọi Devops.

    Nhưng đó là việc của bọn Devops, còn đối với chúng ta, những developer chân đất, sáng ngậm đắng (cà phê), tối nuốt cay (bia rượu), buổi trưa ngồi code thì có cần quan tâm không?

    Xin thưa là có. Bất cứ một công cụ nào sinh ra cũng cần có người xài, và bất cứ ai cũng có thể xài.

    Đứng ở phương diện của một Developer, chúng ta có thể sử dụng Docker và Vagrant để thiết lập môi trường ảo cho một dự án nào đó, mà không cần tốn quá nhiều công sức cài đặt hoặc làm ảnh hưởng tới máy tính hiện tại. Làm thế nào để sử dụng? Làm thế nào để tạo môi trường ảo, và vì sao phải làm như vậy? Xin mời các bạn đừng hỏi nữa, cứ đọc tiếp.

    Disclaimer: Bài viết sau có đề cập đến project của một thành viên của Kipalog nhưng không hề có sự móc nối, PR hay mối tư thù nào giữa tác giả và thành viên này cả :sweat_smile:

    Disclaimer #2: Để tiện cho việc theo dõi bài viết, các bạn không cần có chút khái niệm nào về Docker, nhưng cần cài đặt sẵn nó trên máy tính. Sau bài viết này các bạn sẽ dùng được Docker để làm các công việc đơn giản của một Developer, và có kiến thức nền cơ bản để tìm hiểu sâu hơn về công cụ này. Bài viết sẽ không đề cập tới chuyện so sánh giữa Docker và Vagrant tuy nhiên các bạn có thể comment bên dưới để thảo luận về vấn đề này.

    Disclaimer #3: Dành cho bạn nào chưa biết về Github, đây là một "mạng xã hội" dành cho các Developer, dùng để chia sẽ các dự án mã nguồn mở. Một người tạo project trên đó, up source code của mình lên, các developer khác nếu thích có thể tham gia đóng góp vào project (fix bug, thêm chức năng, sửa code, review code,...), và hoạt động này gọi chung là Contribute, người đóng góp được gọi là Contributor.
    Một contributor muốn contribute cho một project nào đó trên Github thì chỉ việc Fork project đó về, sửa rồi commit, sau đó tạo Pull request để người chủ project biết và sử dụng bản fix của bạn vào project của họ.
    Nếu cảm thấy project hay ho, có thể đánh bấm nút Star để "like" cho project đó.
    Nếu có một project đạt được nhiều stars trên Github thì người developer đó sẽ cảm thấy rất "sướng" =)) giống như mấy bạn hot gơn muốn có được nhiều like trên Facebook vậy.
    Trở thành contributor của một project nhiều sao (stars) cũng rất "sướng".
    Các developer thời nay cũng sống ảo chả khác gì các em hot gơn cả, các bạn ạ...

    Chuyện kể rằng...

    Buổi sáng hôm đó, trời nắng đẹp và se lạnh. Sau khi đánh một giấc đã đời trên xe bus, chàng developer cần mẫn lếch xác vào công ty, việc đầu tiên anh làm là mở máy tính lên và đọc báo, lướt facebook, github,...

    Tình cờ trông thấy một project mới trên github (cái này https://github.com/huydx/scalresume)

    Tò mò vào xem thử, hoá ra lão ấy viết bằng Scala. Tính bỏ qua rồi nhưng cũng muốn contribute chút gì đó cho cái project.

    Thấy README.md của lão viết sơ sài quá, nảy ra ý định add thêm vài dòng hướng dẫn và screenshot cho nó hấp dẫn (đây cũng là cách để trở thành một contributor nhiệt tình trên Github, các bạn chú ý nhá).

    Nhưng hồi nào giờ có làm Scala đâu, chả lẽ vì cái project bé tí này =)) mà phải ngồi cài một đống tool vào máy, nào là Java, nào là Scala, SBT,... để build và chụp cho được cái screenshot sao :joy: Chưa kể có một đống dự án của công ty, chưa hoàn thành, ở trong máy, nếu rủi cài vài rồi hư máy thì biết ăn nói làm sao với sếp.

    Giải pháp

    Thế là nghĩ ngay đến chuyện dùng Docker.
    Đối với các bạn chưa biết Docker là gì, thì đây là một công cụ tạo môi trường được "đóng gói" (còn gọi là Container) trên máy tính mà không làm tác động tới môi trường hiện tại của máy.
    Một số developer thường tạo sẵn các môi trường này, và upload lên mạng để mọi người lấy về dùng, và mấy cái này gọi là các Images
    Đại khái là sẽ làm thế này:

    • Dùng docker tạo một container chứa toàn bộ môi trường develop cần thiết cho Scala
    • Get project về và build, chạy thử
    • Chụp hình add vào README.md
    • Tạo pull request để đóng góp cho cái project này
    • Nghiễm nhiên trở thành contributor dù ko biết chút gì về Scala

    Thực hiện

    Bước 1: Tìm và tải image Docker cho Scala

    Đầu tiên chúng ta cần tìm một image chạy Scala cho Docker. Bước này thì dễ thôi, có thể search trên Google với từ khoá:
    Docker image for Scala
    Yeah, rất nhiều images, chọn đại cái kết quả đầu tiên trên Docker Hub: https://hub.docker.com/r/lukasz/docker-scala/
    Docker Hub là nơi để mọi người upload, chia sẽ các images Docker của mình
    Sau khi tìm ra image rồi, chúng ta tiến hành download nó về, bước này gọi là Pull, chúng ta gõ lệnh sau:
    docker pull lukasz/docker-scala
    Với lukasz/docker-scala là tên của image. Tên này gồm 2 phần, phần đầu trước dấu xoẹt /, lukasz là tên thằng tác giả, và phần sau docker-scala là tên của image.

    Dung lượng của image này hơi lớn một tí (1GB) nên có thể pull hơi lâu, các bạn có thể sử dụng mạng của công ty để quá trình pulling diễn ra nhanh hơn

    Sau khi pull xong, các bạn có thể dùng lệnh sau để xem danh sách các images được pull về sẵn trong máy:
    docker images
    Kết quả trả về như sau:
    REPOSITORY            TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
    ubuntu                latest              91e54dfb1179        6 weeks ago         188.4 MB
    lukasz/docker-scala   latest              90b5825bae47        20 months ago       1.217 GB
    Như kết quả trên thì trong máy mình đang có 2 images: ubuntu và lukasz/docker-scala

    Các images này các bạn chỉ cần tải về 1 lần và để đó dùng lại cho các lần sau được. Nên yên tâm về cái khoản chờ đợi. Trừ khi bạn buồn tình xoá chúng đi :laughing:

    Bước 2: Lấy code từ Github về

    Trước khi tạo môi trường ảo để chạy, chúng ta sẽ lấy code từ Github về trước. Ví dụ mình lấy code về và lưu ở thư mục /Users/madman/Code
    git clone https://github.com/huydx/scalresume.git /Users/madman/Code

    Bước 3: Tạo môi trường ảo Scala liên kết với thư mục Code

    Bây giờ chúng ta sẽ tạo một môi trường ảo, còn gọi là một container sử dụng image Scala vừa pull về ở bước trên.

    Container này liên kết với thư mục code hiện tại của chúng ta, và như thế chúng ta sẽ truy cập được vào thư mục code từ bên trong container.

    Gõ lệnh sau:
    docker run -v /Users/madman/Code:/src -it lukasz/docker-scala /bin/bash
    Ý nghĩa của câu lệnh trên:

    • docker run : lệnh tạo một container cho Docker
    • tham số -v /Users/madman/Code:/src: để liên kết thư mục /Users/madman/Code ở máy tính hiện tại, vào thư mục /src của container.
    • tham số -it lukasz/docker-scala /bin/bash: Để chỉ định images cần tạo là lukasz/docker-scala và tự động chạy lệnh /bin/bash sau khi khởi tạo xong, nhờ đó chúng ta sẽ truy cập được vào chế độ dòng lệnh của container và quản lý nó giống như một máy tính thông thường.

    Bước 4: Sử dụng container để build và chạy code

    Sau khi chạy lệnh run thì cửa sổ terminal của chúng ta thay đổi thành như thế này:
    root@4cc671941ee3:/# 
    Lúc này, bạn đã truy cập vào bên trong container docker-scala.

    Thử gõ lệnh ls để liệt kê các thư mục hiện có:
    ls
    Kết quả:
    root@4cc671941ee3:/# ls
    bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  selinux  src  srv  sys  tmp  usr  var
    root@4cc671941ee3:/#
    Giống y chang một máy tính Linux! Giờ chúng ta thử kiểm tra môi trường hiện tại xem, gõ lệnh:
    scala
    Kết quả trên màn hình:
    root@4cc671941ee3:/# scala
    Welcome to Scala version 2.10.3 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_51).
    Type in expressions to have them evaluated.
    Type :help for more information.
    
    scala>
    Như vậy, chúng ta hiện đang có Scala phiên bản 2.10.3 chạy trên nền Java 1.7.0_51

    Bây giờ chúng ta có thể build và chạy thử ứng dụng Scala vừa lấy về từ Github và tiến hành Contribute cho nó. Về bước này, mình sẽ nói ở một bài viết khác, dài hơn :))

    Để thoát ra khỏi môi trường ảo hiện tại, bạn chỉ cần gõ lệnh:
    exit

    Tổng hợp (dành cho ai thấy dài quá ko đọc):

    Pull một image từ Docker Hub

    docker pull <image name>

    Tạo một container từ image có sẵn

    docker run -v <thư mục trên máy tính>:<thư mục trong container> -it <image name> /bin/bash
    Lệnh trên tạo container, liên kết một thư mục trên máy tính vào bên trong container, và truy cập vào chế độ dòng lệnh của container đó.

    Đối với các ứng dụng như web, container sẽ tạo một web server trên một cổng nào đó, khi đó chúng ta cần phải map cổng đó từ container ra máy tính ngoài, khi đó chúng ta dùng thêm tham số -p như sau:
    docker run -v /abc:/abc -p 80:80 -it ubuntu /bin/bash
    Lệnh trên map cổng 80 của container ra cổng 80 của máy tính hiện tại.

    Ngoài ra, Còn một số lệnh khác mà các bạn có thể dùng để quản lý Docker của mình:

    Liệt kê các images hiện có

    docker images
    Trong kết quả trả về của lệnh này, chúng ta lưu ý các thông số:

    • TAG: là tên của image, ví dụ lukasz/docker-scala
    • IMAGE ID: là ID của image lưu trong hệ thống, ví dụ 91e54dfb1179

    Liệt kê các container đang chạy

    docker ps
    Trong kết quả của lệnh này cũng có các thông số chúng ta cần lưu ý, đó là:

    • CONTAINER ID: Là ID của container đó, ví dụ 4cc671941ee3
    • NAME: Là tên riêng của container, được tạo ra một cách ngẫu nhiên hoặc có thể tự đặt, ví dụ stupefied_blackwell

    Liệt kê toàn bộ các container đang chạy và đã tắt

    Sau khi các bạn thoát khỏi container, nếu nó không còn chạy bất cứ một tiến trình nào nữa, thì nó sẽ tự động tắt, nhưng chưa hoàn toàn bị xoá, khi đó chạy lệnh docker ps sẽ ko thấy được. Để xem lại nó thì chúng ta dùng lệnh sau.
    docker ps -a

    Khởi động và truy cập lại vào một container đã tắt

    Nếu một container đã tắt (không xuất hiện khi dùng lệnh docker ps nữa, chúng ta có thể chạy lệnh docker ps -a để lấy ID hoặc NAME của nó, sau đó dùng lệnh sau để khởi động và truy cập lại vào đó)
    docker start <ID hoặc NAME>
    docker exec -it <ID hoặc NAME> /bin/bash

    Xoá một container

    Nếu một container đã hết giá trị lợi dụng, dù nó đã tắt nhưng nó vẫn chiếm một phần dung lượng trên máy tính, để xoá nó đi, chúng ta dùng lệnh rm
    docker rm <ID hoặc NAME>
    Nếu container đang chạy, bạn cũng có thể xoá nhưng phải thêm tham số -f vào sau rm để force remove:
    docker rm -f <ID hoặc NAME>

    Xoá một image

    Cũng như container, nếu bạn đã ko còn nhu cầu sử dụng một image nào đó nữa, thì nên xoá nó đi, vì dung lượng nó khá là nặng, để lại chật máy. Dùng lệnh rmi (tức là remove image đó)
    docker rmi <ID hoặc NAME>
    hoặc
    docker rmi -f <ID hoặc NAME>
    Trên đây là một số thao tác sử dụng Docker đơn giản, đủ xài đối với 1 developer với mục đích sử dụng Docker làm công cụ tạo môi trường ảo.

    Với các mục đích sử dụng khác, các bạn nên tìm hiểu kĩ hơn về Docker từ những kiến thức cơ bản trong bài viết này.

    Happy Dockering ^^

    Thứ Bảy, 27 tháng 2, 2016

    Cấu hình firewall sử dụng ipset


    Iptables là công cụ để quản lý, cấu hình các quy tắc firewall trong Linux kernel. iptables chỉ là một phần trong netfilter framework, chịu trách nhiệm giao tiếp giữa người dùng và netfilter để đẩy các rule của người dùng vào cho Netfiler xử lý.

    ipset là một phần mở rộng của iptables cho phép bạn tạo các quy tắc tường lửa match với một tập các địa chỉ. Khác với iptables chain được lưu trữ và xử lý tuyến tính, ipset được lữu trữ dưới dạng cấu trúc dữ liệu, hỗ trợ tìm kiếm hiệu quả hơn.

    Bên cạnh những tình huống mà bạn có thể nghĩ ngay tới để áp dụng ipset hiệu quả, chẳng hạn như chặn một số lượng lớn các host riêng lẻ mà không làm ảnh hưởng tới hiệu năng hệ thống hay gây nghẽn mạng, ipset cũng mở ra những cách thức tiếp cận mới khi thiết kế firewal và có thể giúp đơn giản hóa nhiều kịch bản cấu hình.

    Trong bài viết này, chúng ta sẽ thảo luận nhanh về cách cài đặt ipset, một số nguyên tắc và khái niệm cốt lõi về cách thức hoạt động của iptables. Sau đó sẽ tập trung vào cách sử dụng, cú pháp và các ví dụ về kết hợp ipset hoạt động cùng iptables.

    Cài đặt

    ipset chỉ là một package trên hầu hết các distribution, do đó việc cài đặt khá đơn giản, chúng ta sẽ không mất nhiều thời gian ở bước này. Nếu distro của bạn không có sẵn package, hãy thực hiện theo các hướng dẫn cài đặt ở trang chủ http://ipset.netfilter.org/
    Để cài đặt ipset trên Ubuntu, thực hiện lệnh:
    sudo apt-get install ipset
    Để cài đặt ipset trên CentOS, thực hiện lệnh:
    sudo yum install ipset

    Tổng quan iptables

    Một cách ngắn gọn, một iptables firewall là tập hợp các chain, được phân ra 4 bảng chính (raw, mangle, nat, filter), mỗi chain bao gồm một hoặc nhiều các rule. Với mỗi một gói tin, ở từng giai đoạn xử lý, kernel sẽ tham chiếu tới các chain phù hợp để xác định sẽ làm gì với gói tin. Các chain sẽ được tham chiếu theo thứ tự, dựa vào “hướng” của gói tin ( remote-to-local, remote-to-remote hoặc local-to-remote ) . Bảng dưới đây mô tả các chain sẽ được tham chiếu tùy theo hướng cụ thể của gói tin.
    Khi tham chiếu một chain, gói tin sẽ được so sánh với từng rule theo thứ tự trong chain đó cho đến khi tìm được rule phù hợp. Khi tìm được rule phù hợp, gói tin sẽ được xử lý theo target của rule đó. Trong trường hợp không có rule phù hợp, default target hoặc một chính sách nào đó mặc định sẽ được chọn để xử lý gói tin.
    Một chain là tập các rule được sắp xếp theo thứ tự, một rule đơn giản là sự kết hợp giữa match/target, tương tự như chúng ta sử dụng câu điều kiện nếu … thì …
    Một ví dụ đơn giản về match là "TCP destination port 80", một target có thể là “drop the packet”. Target có thể điều hướng packet đến một chain do user tự định nghĩa.
    Một câu lệnh định nghĩa các rule của iptables có 3 phần cơ bản để chỉ rõ table/chain, match và target.

    Bên cạnh các tính năng được tích hợp sẵn, iptables còn cung cấp khả năng mở rộng như một API cho "match extensions" (module để phân loại gói tin ) và "target extensions" (module để xác định sẽ làm gì với gói tin).

    ipset

    ipset là một match extension của iptables. Để sử dụng nó, bạn sẽ tạo các set bằng lệnh ipset, sau đó sử dụng nó để chỉ định match specifition của một rule trong iptables. Một set đơn giản là một danh sách các địa chỉ được lưu trữ tối ưu cho việc tra cứu với tốc độ nhanh.
    Giả sử nếu bạn muốn chặn traffic từ 2 địa chỉ IP là 1.1.1.1 và 2.2.2.2 thì ta sẽ thực hiện 2 lệnh sau với iptables.
    iptables -A INPUT -s 1.1.1.1 -j DROP
    iptables -A INPUT -s 2.2.2.2 -j DROP
    Điều kiện match ở đây -s 1.1.1.1 có nghĩa là các packet có địa chỉ nguồn là 1.1.1.1. Để chặn cả 1.1.1.1 và 2.2.2.2, bạn phải sử dụng 2 rule riêng biệt trong iptables. Bạn hoàn toàn có thể sử dụng ipset để đạt được mục đích tương tự với cách thực hiện như sau:
    ipset -N myset iphash
    ipset -A myset 1.1.1.1
    ipset -A myset 2.2.2.2
    iptables -A INPUT -m set --set myset src -j DROP 
    Các lệnh ipset trên sẽ tạo một set có tên là myset thuộc type iphash gồm 2 địa chỉ 1.1.1.1 và 2.2.2.2. Sau đó iptables sẽ chỉ định match với set được tạo ra từ ipset (-m set --set myset src) , có nghĩa là “match với tất cả các packet có địa chỉ nguồn là một trong các địa chỉ trong myset. Flag src nghĩa là match với địa chỉ nguồn, bạn có thể sử dụng flag “dst” cho mục đích match với địa chỉ đích hoặc sử dụng flag “src,dst” khi cần match cả địa chỉ nguồn và địa chỉ đích.
    Với cách làm sử dụng ipset, bạn chỉ cần tới 1 rule của iptables. Ở ví dụ trên chúng ta chỉ sử dụng 2 địa chỉ IP, nhưng khi cần chặn tới 1000 địa chỉ thì bạn sẽ thấy rõ sự khác biệt. Thay vì sử dụng 1000 rule của iptables, bạn vẫn chỉ cần 1 rule kết hợp cùng ipset để có được tác dụng tương đương.

    Set types

    Mỗi set khi tạo ra sẽ thuộc 1 type xác định, đây là cách để xác định loại giá trị được lưu trữ trong set (IP address, network, port …) và cũng như xác định phần nào của gói tin sẽ được kiểm tra để match với set. Ngoài kiểm tra địa chỉ IP được minh họa ở trên, ipset cũng có thể kiểm tra port, IP + Port và cả địa chỉ MAC + IP của gói tin. Hai dạng linh động nhất thường được sử dụng trong ipsetiphash (lưu trữ danh sách các địa chỉ IP) và nethash (lưu trữ danh sách mạng (IP/mask)). Về các set type bạn có thể tham khảo thêm tại ipset man page.

    Lợi ích của ipset

    Bên cạnh hiệu quả về mặt hiệu năng, ipset cũng cho phép đơn giản hóa cấu hình trong nhiều trường hợp.
    Nếu bạn muốn tường lửa cho phép tất cả các gói tin được tiếp tục xử lý ở chain continue ngoại trừ những gói tin đến từ địa chỉ 1.1.1.1 hoặc 2.2.2.2 , bạn sẽ cấu hình iptables như thế nào ?
    iptables -A INPUT -s ! 1.1.1.1 -g  continue
    iptables -A INPUT -s ! 2.2.2.2 -g  continue
    Cấu hình như trên có hoạt động không ? Tất nhiên là không. Nếu gói tin có địa chỉ nguồn là 1.1.1.1, nó sẽ không match với rule đầu tiên, nhưng gói tin này sẽ match với rule thứ 2 (bởi vì địa chỉ nguồn của nó không phải là 2.2.2.2) nên vẫn được chuyển tới chain “continue”. Đây là một ví dụ đơn giản, nhưng nó cũng minh chứng lợi ích của việc đạt được yêu cầu trong một rule duy nhất. Thực tế các rule của iptables là độc lập với nhau nhưng việc kết hợp, sắp xếp và tối ưu các rule để đạt được những yêu cầu cho trước không phải lúc nào cũng đơn giản, đặc biệt là việc kết hợp các rule bình thường và các rule có điều kiện đảo ngược. ipset sẽ làm cho mọi thứ dễ dàng hơn trong những trường hợp như thế.
    ipset -N myset iphash
    ipset -A myset 1.1.1.1
    ipset -A myset 2.2.2.2
    iptables -A INPUT -m set ! --set myset src -g continue
    Nếu gói tin đi đến có địa chỉ nguồn là 1.1.1.1 hay 2.2.2.2 cũng không thể match với rule (iptables -A INPUT -m set ! --set myset src -g continue ), do đó sẽ không được xử lý tiếp bởi chain “continue”.

    Một lợi ích khác của ipset là bạn có thể thay đổi danh sách địa chỉ của set một cách độc lập với iptables. Đối với iptables, thứ tự các rule là một điểm rất quan trọng, do đó việc sửa đổi các rule hay sắp xếp thứ tự sẽ có ảnh hưởng lớn. Ngược lại, ipset lưu trữ các giá trị trên một mặt phẳng, không đòi hỏi thứ tự nên việc thêm, xóa, sửa đổi không mất nhiều thời gian cân nhắc, suy nghĩ.

    Giới hạn truy cập

    Nếu một ngày boss của bạn nói rằng có một số nhân viên thường xuyên giải trí trên Internet trong giờ hành chính thay vì làm việc, nhiệm vụ khi đó sẽ là giới hạn truy cập trên PC của các nhân viên này chỉ có thể kết nối tới những trang web họ cần để làm việc nhưng không làm ảnh hưởng đến những người khác.
    Để giới hạn 3 PC (10.0.0.5, 10.0.0.6 và 10.0.0.7) chỉ có thể kết nối tới 3 địa chỉ worksite1.com, worksite2.com và worksite3.com, bạn thực hiện như sau :
    ipset -N limited_hosts iphash
    ipset -A limited_hosts 10.0.0.5
    ipset -A limited_hosts 10.0.0.6
    ipset -A limited_hosts 10.0.0.7
    ipset -N allowed_sites iphash
    ipset -A allowed_sites worksite1.com
    ipset -A allowed_sites worksite2.com
    ipset -A allowed_sites worksite3.com
    iptables -I FORWARD -m set --set limited_hosts src -m set ! --set allowed_sites dst -j DROP
    Ví dụ này sử dụng match cả hai set trong cùng 1 rule. Nếu packet đến có địa chỉ nguồn match với  limited_hosts và địa chỉ đích không match với allowed_sites, gói tin sẽ bị DROP.

    Vậy trong tình huống ngược lại, boss muốn chặn truy cập tới một số trang web trên tất cả các host trên mạng LAN, trừ PC của boss và trợ lý. Cách thực hiện sẽ như sau:
    ipset -N blocked_sites iphash  
    ipset -A blocked_sites badsite1.com  
    ipset -A blocked_sites badsite2.com  
    ipset -A blocked_sites badsite3.com
    
    ipset -N allowed_hosts iphash  
    ipset -A allowed_hosts 10.0.0.5  
    ipset -A allowed_hosts 10.0.0.6
    
    iptables -I FORWARD -m set --set ! allowed_hosts src  -m set --set blocked_sites dst -j DROP  
    Lưu ý rằng trong nhiều trường hợp, giải pháp này sẽ không hiệu quả đối với nhiều website. Chẳng hạn bạn muốn chặn truy cập vào facebook.com, thực tế là domain này sẽ được phân giải ra rất nhiều IP, mà iptables/ipset chỉ hỗ trợ hostname nếu chúng được phân giải ra một địa chỉ IP duy nhất . Việc phân giải tên miền cũng chỉ xảy ra lúc thực hiện lệnh, sau đó nếu địa chỉ IP thay đổi, iptables cũng không cập nhật sự thay đổi này. Vì những lý do đó, cách tốt nhất để áp dụng chính sách truy cập Web là sử dụng các giải pháp về HTTP Proxy như Squid.

    Tự động chặn các truy cập không mong muốn

    ipset cũng cung cấp target extension cho iptables có thể tự động thêm hoặc xóa các entry dựa trên iptables rule. Thay vì phải thêm các entry bằng lệnh ipset, bạn có thể cấu hình để iptables thực hiện việc này.
    Giả sử có một host đang muốn kết nối vào port 25 nhưng bạn lại không chạy SMTP server trên máy chủ, và bạn muốn chặn luôn các traffic đến từ host này. Cách thực hiện như sau:
    ipset -N banned_hosts iphash  
    iptables -A INPUT -p tcp --dport 25 -j SET --add-set banned_hosts src  
    iptables -A INPUT -m set --set banned_hosts src -j DROP  
    Nếu gói tin có destination port là 25, địa chỉ nguồn của nó sẽ được thêm vào banned_hosts, sau đó ta sẽ thêm một rule để chặn các gói tin có địa chỉ nguồn nằm trong banned_hosts.

    ipset trong OpenStack

    Neutron sử dụng iptables để thiết lập chức năng security group, nhưng ở các phiên bản trước sẽ gặp phải một số vấn đề. Khi một security group có nhiều rule liên quan đến các security group khác, hiệu năng của security group sẽ giảm hay khi một port được update, toàn bộ các chain liên quan đến port này sẽ được xóa đi và tạo lại, điều này cũng gây ra vấn đề hiệu năng cho L2 agent.
    Trong Openstack Juno, ipset được sử dụng để tối ưu iptables rule chain. Giả sử bạn có 2 instance A và B có địa chỉ IP là 10.3.25.57, 10.3.25.58 thuộc SECGROUP1. Bạn tạo một intances C mới thuộc SECGROUP2. SECGROUP2 sẽ chỉ cho phép kết nối SSH từ các instance thuộc SECGROUP1.
    Ở các phiên bản trước, iptables rule của port thuộc instance C trong L2 agent sẽ như sau :
    -A neutron-openvswi-i9f5f8a14-e -m state --state INVALID -j DROP
    -A neutron-openvswi-i9f5f8a14-e -m state --state RELATED,ESTABLISHED -j RETURN
    -A neutron-openvswi-i9f5f8a14-e -s 10.3.25.3/32 -p udp -m udp --sport 67 --dport 68 -j RETURN
    -A neutron-openvswi-i9f5f8a14-e -p tcp -m tcp --dport 22 -s 10.3.25.57/32 -j RETURN
    -A neutron-openvswi-i9f5f8a14-e -p tcp -m tcp --dport 22 -s 10.3.25.58/32 -j RETURN
    Khi sử dụng “iptables+ipset”, các rule đối với instance C sẽ như sau:
    -A neutron-openvswi-i9f5f8a14-e -m state --state INVALID -j DROP
    -A neutron-openvswi-i9f5f8a14-e -m state --state RELATED,ESTABLISHED -j RETURN
    -A neutron-openvswi-i9f5f8a14-e -s 10.3.25.3/32 -p udp -m udp --sport 67 --dport 68 -j RETURN
    -A neutron-openvswi-i9f5f8a14-e -p tcp -m tcp --dport 22 -m set --match-set IPv49ba654b2-a3a4-49f8-8 src -j RETURN
    -A neutron-openvswi-i9f5f8a14-e -j neutron-openvswi-sg-fallback
    ipset chain sẽ chứa địa chỉ IP của các instance thuộc SECGROUP1
    # ipset list IPv49ba654b2-a3a4-49f8-8
    Name: IPv49ba654b2-a3a4-49f8-8  
    Type: hash:ip  
    Revision: 2  
    Header: family inet hashsize 1024 maxelem 65536  
    Size in memory: 16536  
    References: 1  
    Members:  
    10.3.25.57  
    10.3.25.58
    Bằng cách này, thời gian iptables-save/iptables-load sẽ gỉam đi đáng kể khi chỉ cần chỉnh sửa các ipset trong trường hợp có sự thay đổi về Security Group.

    ipset không chỉ cung cấp thêm một phương thức cấu hình mới cho iptables, nó còn hỗ trợ đơn giản hóa nhiều kịch bản mà nếu chỉ sử dụng iptables sẽ gặp khó khăn hoặc kém hiệu quả. Bạn cũng có thể kết hợp ipset với một số tính năng khác của iptables, chẳng hạn như packet marking, để thực hiện các chính sách về network. Bất cứ lúc nào bạn muốn thiết lập các rule firewall áp dụng cho một nhóm host hoặc nhiều địa chỉ cùng một lúc, bạn nên cân nhắc tới việc kết hợp cùng ipset.

    Tham khảo

    Loadavg

    Phần I : Tổng quan Loadavg

    Ting ..ting.. không phải lương về đâu các bạn. Tiếng tin nhắn cảnh báo đã không còn xa lạ gì với một sysadmin. Login vào server để kiểm tra nguyên nhân. Quái! Sao lần này login lại lâu đến như vậy. Mọi command cũng chậm hơn rất nhiều so với bình thường. Bình tĩnh tìm hiểu kỹ hơn. Tôi giật mình: server của tôi đang có loadavg cao ngất ngưởng.

    Điều này đồng nghĩa với việc server trở nên ì ạch. Việc xử lý process chậm đi đáng kể. Tại sao lại có sự bất thường này? Gãi râu tự hỏi. Vậy:
    • Loadavg là gì và được tính toán như thế nào?
    • Loadavg ở ngưỡng ra sao thì hợp lý?
    • Những yếu tố nào ảnh hưởng đến loadavg?
    • Phương án phân tích và xử lý khi server tải cao thế nào?
    Tôi sẽ trả lời 4 câu hỏi kể trên trong phần I của bài viết về Loadavg này.

    Loadavg là gì và được tính toán như nào?

    Trước tiên bắt đầu với systemload hay còn gọi là load. 
    Systemload thể hiện số công việc hiện tại hệ thống đang thực thi. 
    Một server hoàn toàn nhàn rỗi có load là 0 
    Mỗi tiến trình đang chạy hoặc chờ đợi cpu xử lý sẽ add giá trị 1 vào load 
    Ví dụ với load = 5 => Có 5 process đang chạy hoặc chờ xử lý (Thread running, waiting) 
    Nhưng chúng ta thường nghe đến khái niệm loadavg

    Tại sao không phải là load mà là loadavg???

    Ví dụ thế này để các bạn dễ hình dung:
    • Tại 1 phần trăm giây đầu tiên Load = 0 vì server đang rảnh rỗi
    • Tại phần trăm giây tiếp theo Load = 5 vì thời điểm có 5 proces cần xử lý.
    • Tại phần trăm giây sau đó Load = 99 rất lớn vì thời điểm có rất nhiều process chạy qua hệ thống.
    Các con số Load cho mỗi một thời điểm này không có ý nghĩa nhiều trong việc đánh giá tải của hệ thống. 
    Loadavg thể hiện tải trung bình của hệ thống qua mỗi đoạn thời gian: cho thấy trung bình có bao nhiều process mà server phải thực hiện. 
    Hay nói cách khác: giá trị Loadavg cho ta thấy được trung bình khối lượng công việc hệ thống phải xử lý trong mỗi khoảng thời gian: 1 phút, 5 phút và 15 phút.
    cat /proc/loadavg
    3.00 5.00 4.00
    Hiểu giá trị console này như sau: 
    Trong 1 phút gần đây trung bình có 3 process cần được xử lý (3 thread running, waiting) 
    Tương tự như vậy có trung bình 5 process xử lý trong vòng 5 phút và 4 process xử lý trong vòng 15 phút.

    Loadavg ở ngưỡng ra sao thì hợp lý?

    Loadavg có một ngưỡng chung? 
    Các server đều có một ngưỡng loadavg cố định? 
    Câu trả lời là Không 
    Điều này còn phụ thuộc vào server có bao nhiêu CPU. Có thể xem số CPU của sever bằng lệnh sau:
    cat /sys/devices/system/cpu/online
    0-3
    Như vậy server hiện tại (bao gồm cả hyper-v threading) có 4 CPUs 
    Giả sử với mỗi CPU là 1 cây cầu. Process qua mỗi CPU thể hiện như sau:

    Với 4 CPUs chúng ta có 4 cây cầu và có thể xử lý với mức Loadavg <= 4.00 là mức lý tưởng.

    Để thấy rõ hơn điều này ta thực hiện 1 vài thử nghiệm với server 4 CPUs. Chúng ta cùng xem nó xử lý process như nào khi loadavg lần lượt bằng: 1.00, 4.00 và 8.00
    • Case 1: Server 4 CPUs với loadavg = 1.00
    Process vừa tạo được hoàn toàn sử dụng CPU với tốc độ xử lý bình thường, performance đạt 100%. Loadavg lúc này tăng lên 1.00.
    • Case 2: Server 4 CPUs với loadavg = 4.00

    4 process chạy đồng thời đang phân chia sử dụng 4 CPUs mà server đang có. Tốc độ xử lý vẫn đạt 100%. Loadavg lúc này đã tăng lên 4.00
    • Case 3: Server 4 CPUs với loadavg = 8.00

    Loadavg thời điểm này lên tới 8.00 gấp đôi số CPU hiện có. Mỗi process chỉ còn sử dụng được 50% CPU => Tốc độ xử lý chậm đi tương ứng.
    Qua hàng loạt case test, ta thấy loadavg nên duy trì nhỏ hơn hoặc bằng số CPU. Process càng vượt quá CPU hiện có bao nhiêu, tốc độ xử lý càng chậm đi tương ứng.

    Những yếu tố nào ảnh hưởng đến loadavg?

    Vậy điều gì làm tải của hệ thống tăng cao.
    Như 1 vài case test ở mục 2 cho thấy mối quan hệ giữa CPU utilization và loadavg

    Nhưng liệu loadavg có chỉ phụ thuộc vào CPU utilization?

    Tải của hệ thống có thể được tính toán (count) dựa trên các tiến trình đang được xử lý (running on CPU) và các tiến trình runable (waiting for CPU)
    Ngoài ra tải còn bao gồm các tiến trình uninterruptible sleep states (waiting disk I/O hoặc network). Những tiến trình này cũng góp phần làm tăng cao tải hệ thống mặc dù nó không thực sự sử dụng CPU.
    --> Có 3 yếu tố góp phần làm tăng tải hệ thống:
    • Cpu Utilazion
    • Disk I/O
    • Network Traffic
    Việc phân tích sự ảnh hưởng của các yếu tố này sẽ khá dài và phức tạp, chúng ta sẽ cùng nhau tìm hiểu sâu thêm trong những phần sau. Giờ tôi sẽ hướng dẫn bạn cách phân tích và xử lý khi server gặp tải cao.

    Phương án phân tích và xử lý khi server tải cao như nào?

    Chúng ta đã thống nhất với nhau ba yếu tố:
    • Cpu Utilazion
    • Disk I/O
    • Network Traffic
    đều có thể gây ảnh hưởng đến tải của hệ thống.

    Cần xử lý những gì khi loadavg lên quá cao?

    Ta cần phải phân tích yếu tố nào gây tải cao hệ thống.
    Trước hết cần phải theo dõi CPU utilization.
    • Nếu lượng CPU utilization lớn hơn 100% và Loadavg vượt quá số CPU đang có. 
    --> Có thể kết luận loadavg cao bởi lượng lớn các process đang running hoặc waiting cpu xử lý.
    Sử dụng top -i để theo dõi các process running phân tích các process và đưa phương án.
    • Nếu lượng cpu sử dụng vẫn bình thường (tức không quá cao) nhưng loadavg vẫn cao hơn số cpu đang có. 
    --> Vậy có thể kết luận có thể disk I/O hoặc network traffic hoặc cả hai là yếu tố chính gây ra tải cao cho hệ thống. 
    Sử dụng 1 số tool linux cung cấp như iotop, atop, vmstat để có thể phân tích chính xác yếu tố nào và đưa ra phương án xử lý.

    Phần II : Các yếu tố ảnh hưởng tới Loadavg

    1. CPU utilization và load average

    Trong trường hợp CPU của bạn được dùng cho những tính toán rất nhẹ nhàng có thể xong tức thì nhưng số lượng process cần CPU lại rất cao, các process cần xử lý tại một thời điểm vượt mức CPU core server hiện có. Điều này nói lên rằng CPU của bạn đang bị quá tải process. Có nhiều lý do dẫn đến trường hợp này và mỗi trường hợp có nhiều cách giải quyết khác nhau.

    Một ví dụ hay thấy trường hợp này là máy chủ web. Việc render các trang web là không hề nặng, tuy vậy với các máy chủ web chịu trafic lớn (số lượng connection lớn), các process phục vụ request sẽ phải xếp hàng dẫn đến tình trạng trang web bị phục vụ với thời gian kéo dài hơn.

    Tuy nhiên trong quá trình vận hành hệ thống, không ít lần tải hệ thống lên rất cao mặc dù các tiến trình không thực sự sử dụng nhiều CPU. Vậy là lúc chúng ta xem xét yếu tố tiếp theo gây ra tải cao hệ thống.

    2. Disk I/O và load average

    Trong phần này ta sẽ cùng nhau làm rõ một số vấn đề :
    • I/O wait ảnh hưởng loadavg như nào?
    • Khi nào I/O wait xảy ra?
    • Mối quan hệ I/O wait và CPU?
    • Khi nào thì high I/O wait?
    I/O wait là giá trị thời gian mà một CPU (hoặc tất cả các CPU) idle bởi vì các tiến trình Runnable đang chờ đợi một hoạt động I/O disk được hoàn thành trong khoảng thời gian nhất định.
    I/O wait = ((CPU waiting on disk time)/ periods) * 100% 
    Một trường hợp hay gặp trong database server:

    Máy chủ DB dành thời gian chủ yếu đợi thao tác vào ra (I/O) như khi truy vấn cơ sở dữ liệu. Số lượng query lớn, số lượng truy vấn cần sắp xếp lớn nhưng dữ liệu cần sắp xếp lại rất bé, thời gian đợi dữ liệu từ disk lại cao. Vì vậy phần lớn CPU sẽ idle, nhưng loadavg vẫn cao.

    Rối tung lên phải không nào? Để tôi ví dụ cụ thể cho các bạn dễ hình dung:

    Trong một truy vấn tốn thời gian là 1s để lấy 10.000 rows và thực hiện 1 số thao tác với các row đó:


    Process đi vào CPU để xử lý. CPU sẽ truy cập vào disk để lấy thông tin các rows. Tại thời điểm này CPU sẽ idle và chờ disk phản hồi, đây chính là thời điểm waiting on disk.

    Như ảnh trên waiting on disk sẽ tốn 700ms trong 1s. I/O wait tại thời điểm này đo được là 70%.

    Đến đây chắc hản các bạn cũng giống tôi thắc mắc ngưỡng của I/O wait. Vậy như nào là high I/O wait?

    High I/O wait phụ thuộc vào số lượng CPU server đang có. 50% iowait của server 2 cpu chỉ tươg đương với 12.5% iowait trên server có 8 cpu. Tỉ lệ nghịch với số lượng cpu của server.


    Tổng quát lại : nếu phần trăm I/O wait lớn (1/số lượng cpu) * 100% thì đây là lúc bạn cần phải xem xét lại disk I/O hệ thống của mình.

    3. Network traffic và load average

    Đã khi nào bạn gặp trường hợp tải hệ thống vẫn rất cao mặc dù I/O wait và CPU utilization tương đối thấp chưa? Nếu rồi thì có lẽ (có lẽ thôi nhé) hệ thống của bạn đang gặp phải trường hợp thứ 3 process waiting for Network I/O

    Tôi sẽ thử vài ví dụ để chứng minh network cũng gây ảnh hưởng đến loadavg:


    Thực hiện traffic nhận 50 nghìn package với khoảng 1,5Gb data qua lo interface. Ta cùng theo dõi CPU utilization và run-queue utilization.

    CPU sử dụng 21% ở user mode và 79% ở kernel mode. Bởi vì tại thời điểm này linux kernel đang phaỉ làm việc rất nhiều để xử lý lượng lớn traffic.

    Bảng thống kê loadavg hiện đang show ta thấy trung bình có 13 process trong run-queue và loadavg-1 đang sắp đạt mốc 13. Đây chính là dấu hiệu cho thấy loavg bị ảnh hưởng bởi quá trình thực hiện network I/O. Các process network đã sử dụng 1 lượng lớn thời gian của CPU và buộc loadavg tăng lên. Ta sẽ xác nhận điều này bằng các giá trị show trong top util


    Như đã thấy các process network stress đã không thực sự sử dụng quá nhiều CPU. Việc tiêu thụ cpu chủ yếu là si = 50% cho thấy cpu utilization chủ yếu cho việc xử lý software interrupts.

    Chúng ta đã cùng phân tích một số các yếu tố ảnh hưởng loadavg, tuy nhiên điều này có thể vẫn còn chưa đủ. Bản chất Loadavg được tính toán dựa trên số lượng process chờ đợi trong run-queue. Do đó ngoài các process đang được xử lý, tải hệ thống vẫn tiếp tục tăng khi bạn có một hoặc một vài process trạng thái UNINTERRUPTIBLE_SLEEP đang chờ đợi thành phần khác như hardware hoặc software để tiếp tục xử lý.

    Linux includes processes in uninterruptible sleep in its load calculation. Such processes show with State 'D' in the usual process inspection tools. This state is usually used by device drivers waiting for disk or network IO. That "usual explanation" is true for Linux, but not most other unixes

    Link tham khảo