Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Hiệu suất cao là một trong những yêu cầu chính khi làm việc với dữ liệu lớn. Trong bộ phận tải dữ liệu tại Sberbank, chúng tôi bơm hầu hết tất cả các giao dịch vào Đám mây dữ liệu dựa trên Hadoop của mình và do đó xử lý các luồng thông tin thực sự lớn. Đương nhiên, chúng tôi luôn tìm cách cải thiện hiệu suất và bây giờ chúng tôi muốn cho bạn biết cách chúng tôi quản lý để vá lỗi RegionServer HBase và máy khách HDFS, nhờ đó chúng tôi có thể tăng đáng kể tốc độ hoạt động đọc.
Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Tuy nhiên, trước khi chuyển sang bản chất của các cải tiến, cần nói về những hạn chế mà về nguyên tắc không thể vượt qua nếu bạn ngồi trên ổ cứng HDD.

Tại sao ổ cứng và đọc truy cập ngẫu nhiên nhanh không tương thích
Như bạn đã biết, HBase và nhiều cơ sở dữ liệu khác lưu trữ dữ liệu theo khối có kích thước vài chục kilobyte. Theo mặc định nó là khoảng 64 KB. Bây giờ hãy tưởng tượng rằng chúng ta chỉ cần lấy 100 byte và chúng ta yêu cầu HBase cung cấp cho chúng ta dữ liệu này bằng một khóa nhất định. Vì kích thước khối trong HFiles là 64 KB nên yêu cầu sẽ lớn hơn 640 lần (chỉ một phút!) so với mức cần thiết.

Tiếp theo, vì yêu cầu sẽ đi qua HDFS và cơ chế lưu trữ siêu dữ liệu của nó Bộ đệm ngắn mạch (cho phép truy cập trực tiếp vào các tệp), điều này dẫn đến việc đọc được 1 MB từ đĩa. Tuy nhiên điều này có thể được điều chỉnh bằng tham số dfs.client.read.short Circuit.buffer.size và trong nhiều trường hợp, việc giảm giá trị này xuống còn 126 KB là điều hợp lý.

Giả sử chúng tôi làm điều này, nhưng ngoài ra, khi chúng tôi bắt đầu đọc dữ liệu thông qua java api, chẳng hạn như các hàm như FileChannel.read và yêu cầu hệ điều hành đọc lượng dữ liệu được chỉ định, nó sẽ đọc "đề phòng" nhiều hơn 2 lần , I E. 256 KB trong trường hợp của chúng tôi. Điều này là do java không có cách dễ dàng để đặt cờ FADV_RANDOM nhằm ngăn chặn hành vi này.

Kết quả là, để có được 100 byte của chúng tôi, chúng tôi phải đọc gấp 2600 lần. Có vẻ như giải pháp là hiển nhiên, hãy giảm kích thước khối xuống còn kilobyte, đặt cờ được đề cập và đạt được khả năng tăng tốc khai sáng tuyệt vời. Nhưng vấn đề là bằng cách giảm kích thước khối xuống 2 lần, chúng ta cũng giảm số byte được đọc trên một đơn vị thời gian xuống 2 lần.

Có thể thu được một số lợi ích từ việc đặt cờ FADV_RANDOM, nhưng chỉ với tính năng đa luồng cao và với kích thước khối là 128 KB, nhưng đây là mức tối đa vài chục phần trăm:

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Các thử nghiệm được thực hiện trên 100 tệp, mỗi tệp có kích thước 1 GB và nằm trên 10 ổ cứng.

Về nguyên tắc, hãy tính toán những gì chúng ta có thể tin tưởng ở tốc độ này:
Giả sử chúng ta đọc từ 10 đĩa với tốc độ 280 MB/giây, tức là. 3 triệu lần 100 byte. Nhưng như chúng tôi nhớ, dữ liệu chúng tôi cần ít hơn 2600 lần so với dữ liệu được đọc. Vì vậy, chúng tôi chia 3 triệu cho 2600 và nhận được 1100 bản ghi mỗi giây

Thật chán nản phải không? Đó là bản chất Truy cập ngẫu nhiên truy cập dữ liệu trên ổ cứng - bất kể kích thước khối. Đây là giới hạn vật lý của quyền truy cập ngẫu nhiên và không có cơ sở dữ liệu nào có thể hoạt động hiệu quả hơn trong những điều kiện như vậy.

Làm thế nào để cơ sở dữ liệu đạt được tốc độ cao hơn nhiều? Để trả lời câu hỏi này, chúng ta hãy xem điều gì đang xảy ra trong bức ảnh sau:

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Ở đây chúng ta thấy rằng trong vài phút đầu tiên, tốc độ thực sự là khoảng một nghìn bản ghi mỗi giây. Tuy nhiên, hơn nữa, do thực tế là có nhiều dữ liệu được đọc hơn mức yêu cầu, dữ liệu sẽ kết thúc trong bộ đệm/bộ nhớ đệm của hệ điều hành (linux) và tốc độ tăng lên mức khá hơn 60 nghìn mỗi giây.

Do đó, hơn nữa, chúng tôi sẽ giải quyết vấn đề chỉ tăng tốc truy cập vào dữ liệu trong bộ đệm của hệ điều hành hoặc nằm trong các thiết bị lưu trữ SSD/NVMe có tốc độ truy cập tương đương.

Trong trường hợp của chúng tôi, chúng tôi sẽ tiến hành thử nghiệm trên một nhóm gồm 4 máy chủ, mỗi máy chủ được tính phí như sau:

CPU: Xeon E5-2680 v4 @ 2.40GHz 64 luồng.
Bộ nhớ: 730GB.
phiên bản java: 1.8.0_111

Và điểm mấu chốt ở đây là lượng dữ liệu trong các bảng cần đọc. Thực tế là nếu bạn đọc dữ liệu từ một bảng hoàn toàn được đặt trong bộ đệm HBase, thì nó thậm chí sẽ không đọc được từ bộ đệm/bộ đệm của hệ điều hành. Bởi vì HBase theo mặc định phân bổ 40% bộ nhớ cho cấu trúc có tên BlockCache. Về cơ bản, đây là ConcurrentHashMap, trong đó khóa là tên tệp + phần bù của khối và giá trị là dữ liệu thực tế ở phần bù này.

Vì vậy, khi chỉ đọc từ cấu trúc này, chúng ta chúng tôi thấy tốc độ tuyệt vời, chẳng hạn như một triệu yêu cầu mỗi giây. Nhưng hãy tưởng tượng rằng chúng ta không thể phân bổ hàng trăm gigabyte bộ nhớ chỉ cho nhu cầu cơ sở dữ liệu, bởi vì có rất nhiều thứ hữu ích khác đang chạy trên các máy chủ này.

Ví dụ: trong trường hợp của chúng tôi, dung lượng BlockCache trên một RS là khoảng 12 GB. Chúng tôi đã hạ cánh hai RS trên một nút, tức là. 96 GB được phân bổ cho BlockCache trên tất cả các nút. Và dữ liệu còn nhiều hơn gấp nhiều lần, chẳng hạn cho nó là 4 bảng, mỗi bảng 130 vùng, trong đó các tệp có kích thước 800 MB, được nén bằng FAST_DIFF, tức là. tổng cộng là 410 GB (đây là dữ liệu thuần túy, tức là không tính đến hệ số sao chép).

Do đó, BlockCache chỉ chiếm khoảng 23% tổng khối lượng dữ liệu và điều này gần với điều kiện thực tế của cái được gọi là BigData. Và đây là lúc niềm vui bắt đầu - bởi vì rõ ràng, càng có ít lượt truy cập bộ nhớ đệm thì hiệu suất càng kém. Suy cho cùng, nếu trượt, bạn sẽ phải làm rất nhiều việc - tức là. đi xuống gọi các chức năng hệ thống. Tuy nhiên, điều này là không thể tránh khỏi, vì vậy chúng ta hãy nhìn vào một khía cạnh hoàn toàn khác - điều gì xảy ra với dữ liệu bên trong bộ đệm?

Hãy đơn giản hóa tình huống và giả sử rằng chúng ta có bộ đệm chỉ phù hợp với 1 đối tượng. Dưới đây là ví dụ về điều gì sẽ xảy ra khi chúng tôi cố gắng làm việc với khối lượng dữ liệu lớn hơn 3 lần so với bộ đệm, chúng tôi sẽ phải:

1. Đặt khối 1 vào bộ đệm
2. Xóa khối 1 khỏi bộ đệm
3. Đặt khối 2 vào bộ đệm
4. Xóa khối 2 khỏi bộ đệm
5. Đặt khối 3 vào bộ đệm

5 hành động đã hoàn thành! Tuy nhiên, tình trạng này không thể gọi là bình thường, thực chất là chúng ta đang ép HBase làm một đống việc hoàn toàn vô ích. Nó liên tục đọc dữ liệu từ bộ đệm của hệ điều hành, đặt nó vào BlockCache, chỉ để loại bỏ nó gần như ngay lập tức vì một phần dữ liệu mới đã đến. Hình ảnh động ở đầu bài viết cho thấy bản chất của vấn đề - Người thu gom rác đang hoạt động quá quy mô, bầu không khí nóng lên, cô bé Greta ở Thụy Điển xa xôi và nóng nực đang trở nên khó chịu. Và chúng tôi, những người làm CNTT thực sự không thích khi trẻ em buồn, vì vậy chúng tôi bắt đầu suy nghĩ xem mình có thể làm gì với điều đó.

Điều gì sẽ xảy ra nếu bạn không đặt tất cả các khối vào bộ đệm mà chỉ đặt một tỷ lệ phần trăm nhất định trong số chúng để bộ đệm không bị tràn? Hãy bắt đầu bằng cách chỉ thêm một vài dòng mã vào đầu hàm để đưa dữ liệu vào BlockCache:

  public void cacheBlock(BlockCacheKey cacheKey, Cacheable buf, boolean inMemory) {
    if (cacheDataBlockPercent != 100 && buf.getBlockType().isData()) {
      if (cacheKey.getOffset() % 100 >= cacheDataBlockPercent) {
        return;
      }
    }
...

Vấn đề ở đây là như sau: offset là vị trí của khối trong tệp và các chữ số cuối cùng của nó được phân bổ ngẫu nhiên và đồng đều từ 00 đến 99. Do đó, chúng ta sẽ chỉ bỏ qua những khối nằm trong phạm vi chúng ta cần.

Ví dụ: đặt cacheDataBlockPercent = 20 và xem điều gì sẽ xảy ra:

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Kết quả là rõ ràng. Trong các biểu đồ bên dưới, có thể thấy rõ lý do tại sao lại xảy ra sự tăng tốc như vậy - chúng tôi tiết kiệm rất nhiều tài nguyên GC mà không thực hiện công việc Sisyphean là đặt dữ liệu vào bộ nhớ đệm chỉ để ngay lập tức ném nó xuống cống của lũ chó sao Hỏa:

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Đồng thời, mức sử dụng CPU tăng lên nhưng thấp hơn nhiều so với năng suất:

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Cũng cần lưu ý rằng các khối được lưu trữ trong BlockCache là khác nhau. Hầu hết, khoảng 95%, là dữ liệu. Và phần còn lại là siêu dữ liệu, chẳng hạn như bộ lọc Bloom hoặc LEAF_INDEX và т.д.. Dữ liệu này chưa đủ nhưng rất hữu ích, vì trước khi truy cập trực tiếp vào dữ liệu, HBase sẽ xem xét meta để hiểu liệu có cần tìm kiếm thêm ở đây hay không và nếu có thì chính xác khối quan tâm nằm ở đâu.

Do đó, trong đoạn mã chúng ta thấy một điều kiện kiểm tra buf.getBlockType().isData() và nhờ meta này, chúng tôi sẽ để nó trong bộ đệm trong mọi trường hợp.

Bây giờ, hãy tăng tải và thắt chặt tính năng một chút trong một lần. Trong thử nghiệm đầu tiên, chúng tôi đã đưa ra tỷ lệ phần trăm giới hạn = 20 và BlockCache hơi ít được sử dụng đúng mức. Bây giờ, hãy đặt nó thành 23% và thêm 100 luồng cứ sau 5 phút để xem điểm nào xảy ra bão hòa:

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Ở đây chúng ta thấy rằng phiên bản gốc gần như ngay lập tức đạt mức trần với khoảng 100 nghìn yêu cầu mỗi giây. Trong khi đó bản vá cho khả năng tăng tốc lên tới 300 nghìn. Đồng thời, rõ ràng là việc tăng tốc hơn nữa không còn quá “miễn phí” nữa, việc sử dụng CPU cũng ngày càng tăng lên.

Tuy nhiên, đây không phải là một giải pháp quá hay, vì chúng ta không biết trước bao nhiêu phần trăm khối cần được lưu vào bộ nhớ đệm, điều này phụ thuộc vào cấu hình tải. Do đó, một cơ chế đã được triển khai để tự động điều chỉnh thông số này tùy thuộc vào hoạt động của thao tác đọc.

Ba tùy chọn đã được thêm vào để kiểm soát điều này:

hbase.lru.cache.heavy.eviction.count.limit — đặt số lần quá trình xóa dữ liệu khỏi bộ đệm sẽ chạy trước khi chúng tôi bắt đầu sử dụng tính năng tối ưu hóa (tức là bỏ qua các khối). Theo mặc định, nó bằng MAX_INT = 2147483647 và trên thực tế có nghĩa là tính năng này sẽ không bao giờ bắt đầu hoạt động với giá trị này. Bởi vì quá trình trục xuất bắt đầu cứ sau 5 - 10 giây (tùy thuộc vào tải) và 2147483647 * 10/60/60/24/365 = 680 năm. Tuy nhiên, chúng ta có thể đặt tham số này về 0 và làm cho tính năng này hoạt động ngay sau khi khởi chạy.

Tuy nhiên, cũng có một tải trọng trong tham số này. Nếu tải của chúng tôi đến mức các lần đọc ngắn hạn (giả sử vào ban ngày) và các lần đọc dài hạn (vào ban đêm) liên tục xen kẽ thì chúng tôi có thể đảm bảo rằng tính năng này chỉ được bật khi các hoạt động đọc dài đang diễn ra.

Ví dụ: chúng tôi biết rằng các bài đọc ngắn hạn thường kéo dài khoảng 1 phút. Không cần thiết phải bắt đầu loại bỏ các khối, bộ đệm sẽ không có thời gian để trở nên lỗi thời và sau đó chúng ta có thể đặt tham số này bằng, chẳng hạn như 10. Điều này sẽ dẫn đến thực tế là quá trình tối ưu hóa sẽ chỉ bắt đầu hoạt động khi kéo dài- thời hạn đọc tích cực đã bắt đầu, tức là trong 100 giây. Do đó, nếu chúng ta đọc trong thời gian ngắn, thì tất cả các khối sẽ đi vào bộ đệm và sẽ có sẵn (ngoại trừ những khối sẽ bị thuật toán tiêu chuẩn loại bỏ). Và khi chúng tôi thực hiện các lần đọc dài hạn, tính năng này sẽ được bật và chúng tôi sẽ có hiệu suất cao hơn nhiều.

hbase.lru.cache.heavy.eviction.mb.size.limit — đặt số megabyte mà chúng tôi muốn đặt vào bộ đệm (và tất nhiên là loại bỏ) trong 10 giây. Tính năng này sẽ cố gắng đạt được giá trị này và duy trì nó. Vấn đề là thế này: nếu chúng ta nhét gigabyte vào bộ đệm, thì chúng ta sẽ phải loại bỏ gigabyte, và điều này, như chúng ta đã thấy ở trên, rất tốn kém. Tuy nhiên, bạn không nên cố gắng đặt nó quá nhỏ, vì điều này sẽ khiến chế độ bỏ khối thoát sớm. Đối với các máy chủ mạnh (khoảng 20-40 lõi vật lý) thì nên đặt khoảng 300-400 MB là tối ưu. Dành cho tầng lớp trung lưu (~10 lõi) 200-300 MB. Đối với các hệ thống yếu (2-5 lõi), 50-100 MB có thể là bình thường (chưa được thử nghiệm trên các hệ thống này).

Hãy xem cách thức hoạt động của nó: giả sử chúng ta đặt hbase.lru.cache.heavy.eviction.mb.size.limit = 500, có một số loại tải (đọc) và sau đó cứ sau ~ 10 giây chúng ta sẽ tính xem có bao nhiêu byte bị loại bỏ bằng công thức:

Chi phí chung = Tổng số byte được giải phóng (MB) * 100 / Giới hạn (MB) - 100;

Nếu trên thực tế 2000 MB đã bị loại bỏ thì Chi phí chung sẽ bằng:

2000 * 100/500 - 100 = 300%

Các thuật toán cố gắng duy trì không quá vài chục phần trăm, do đó tính năng này sẽ giảm tỷ lệ khối được lưu trong bộ nhớ đệm, từ đó thực hiện cơ chế tự động điều chỉnh.

Tuy nhiên, nếu tải giảm xuống, giả sử chỉ có 200 MB bị loại bỏ và Chi phí chung trở nên âm (được gọi là vượt mức):

200 * 100/500 - 100 = -60%

Ngược lại, tính năng này sẽ tăng tỷ lệ khối được lưu trong bộ nhớ đệm cho đến khi Overhead trở thành dương.

Dưới đây là một ví dụ về cách điều này trông như thế nào trên dữ liệu thực. Không cần phải cố đạt tới 0%, điều đó là không thể. Nó rất tốt khi ở mức khoảng 30 - 100%, điều này giúp tránh việc thoát sớm khỏi chế độ tối ưu hóa trong các đợt tăng ngắn hạn.

hbase.lru.cache.heavy.eviction.overhead.cofactor — đặt tốc độ chúng tôi muốn nhận được kết quả. Nếu chúng tôi biết chắc chắn rằng các lần đọc của chúng tôi chủ yếu dài và không muốn chờ đợi, chúng tôi có thể tăng tỷ lệ này và đạt hiệu suất cao nhanh hơn.

Ví dụ: chúng tôi đặt hệ số này = 0.01. Điều này có nghĩa là Chi phí chung (xem ở trên) sẽ được nhân với con số này với kết quả thu được và phần trăm khối được lưu trong bộ nhớ đệm sẽ giảm xuống. Giả sử rằng Overhead = 300% và hệ số = 0.01 thì tỷ lệ khối được lưu trong bộ nhớ đệm sẽ giảm 3%.

Logic “Back Pressure” tương tự cũng được triển khai cho các giá trị Overhead âm (vượt mức). Vì luôn có thể xảy ra những biến động ngắn hạn về số lượng lượt đọc và số lượt đọc, nên cơ chế này cho phép bạn tránh việc thoát sớm khỏi chế độ tối ưu hóa. Áp suất ngược có logic đảo ngược: mức vượt mức càng mạnh thì càng có nhiều khối được lưu vào bộ nhớ đệm.

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Mã thực hiện

        LruBlockCache cache = this.cache.get();
        if (cache == null) {
          break;
        }
        freedSumMb += cache.evict()/1024/1024;
        /*
        * Sometimes we are reading more data than can fit into BlockCache
        * and it is the cause a high rate of evictions.
        * This in turn leads to heavy Garbage Collector works.
        * So a lot of blocks put into BlockCache but never read,
        * but spending a lot of CPU resources.
        * Here we will analyze how many bytes were freed and decide
        * decide whether the time has come to reduce amount of caching blocks.
        * It help avoid put too many blocks into BlockCache
        * when evict() works very active and save CPU for other jobs.
        * More delails: https://issues.apache.org/jira/browse/HBASE-23887
        */

        // First of all we have to control how much time
        // has passed since previuos evict() was launched
        // This is should be almost the same time (+/- 10s)
        // because we get comparable volumes of freed bytes each time.
        // 10s because this is default period to run evict() (see above this.wait)
        long stopTime = System.currentTimeMillis();
        if ((stopTime - startTime) > 1000 * 10 - 1) {
          // Here we have to calc what situation we have got.
          // We have the limit "hbase.lru.cache.heavy.eviction.bytes.size.limit"
          // and can calculte overhead on it.
          // We will use this information to decide,
          // how to change percent of caching blocks.
          freedDataOverheadPercent =
            (int) (freedSumMb * 100 / cache.heavyEvictionMbSizeLimit) - 100;
          if (freedSumMb > cache.heavyEvictionMbSizeLimit) {
            // Now we are in the situation when we are above the limit
            // But maybe we are going to ignore it because it will end quite soon
            heavyEvictionCount++;
            if (heavyEvictionCount > cache.heavyEvictionCountLimit) {
              // It is going for a long time and we have to reduce of caching
              // blocks now. So we calculate here how many blocks we want to skip.
              // It depends on:
             // 1. Overhead - if overhead is big we could more aggressive
              // reducing amount of caching blocks.
              // 2. How fast we want to get the result. If we know that our
              // heavy reading for a long time, we don't want to wait and can
              // increase the coefficient and get good performance quite soon.
              // But if we don't sure we can do it slowly and it could prevent
              // premature exit from this mode. So, when the coefficient is
              // higher we can get better performance when heavy reading is stable.
              // But when reading is changing we can adjust to it and set
              // the coefficient to lower value.
              int change =
                (int) (freedDataOverheadPercent * cache.heavyEvictionOverheadCoefficient);
              // But practice shows that 15% of reducing is quite enough.
              // We are not greedy (it could lead to premature exit).
              change = Math.min(15, change);
              change = Math.max(0, change); // I think it will never happen but check for sure
              // So this is the key point, here we are reducing % of caching blocks
              cache.cacheDataBlockPercent -= change;
              // If we go down too deep we have to stop here, 1% any way should be.
              cache.cacheDataBlockPercent = Math.max(1, cache.cacheDataBlockPercent);
            }
          } else {
            // Well, we have got overshooting.
            // Mayby it is just short-term fluctuation and we can stay in this mode.
            // It help avoid permature exit during short-term fluctuation.
            // If overshooting less than 90%, we will try to increase the percent of
            // caching blocks and hope it is enough.
            if (freedSumMb >= cache.heavyEvictionMbSizeLimit * 0.1) {
              // Simple logic: more overshooting - more caching blocks (backpressure)
              int change = (int) (-freedDataOverheadPercent * 0.1 + 1);
              cache.cacheDataBlockPercent += change;
              // But it can't be more then 100%, so check it.
              cache.cacheDataBlockPercent = Math.min(100, cache.cacheDataBlockPercent);
            } else {
              // Looks like heavy reading is over.
              // Just exit form this mode.
              heavyEvictionCount = 0;
              cache.cacheDataBlockPercent = 100;
            }
          }
          LOG.info("BlockCache evicted (MB): {}, overhead (%): {}, " +
            "heavy eviction counter: {}, " +
            "current caching DataBlock (%): {}",
            freedSumMb, freedDataOverheadPercent,
            heavyEvictionCount, cache.cacheDataBlockPercent);

          freedSumMb = 0;
          startTime = stopTime;
       }

Bây giờ chúng ta hãy xem xét tất cả điều này bằng một ví dụ thực tế. Chúng tôi có kịch bản thử nghiệm sau:

  1. Hãy bắt đầu thực hiện Quét (25 chủ đề, đợt = 100)
  2. Sau 5 phút, thêm nhiều lượt nhận (25 chủ đề, lô = 100)
  3. Sau 5 phút tắt multi-get (chỉ còn scan lại)

Chúng tôi thực hiện hai lần chạy, đầu tiên hbase.lru.cache.heavy.eviction.count.limit = 10000 (thực tế là tắt tính năng này), sau đó đặt limit = 0 (bật tính năng này).

Trong nhật ký bên dưới, chúng tôi thấy cách bật tính năng này và đặt lại Khả năng vượt mức thành 14-71%. Đôi khi tải giảm, điều này sẽ bật Back Pressure và HBase lưu lại nhiều khối hơn.

Đăng nhập vùngMáy chủ
bị trục xuất (MB): 0, tỷ lệ 0.0, chi phí (%): -100, bộ đếm trục xuất nặng: 0, bộ nhớ đệm hiện tại DataBlock (%): 100
bị trục xuất (MB): 0, tỷ lệ 0.0, chi phí (%): -100, bộ đếm trục xuất nặng: 0, bộ nhớ đệm hiện tại DataBlock (%): 100
bị trục xuất (MB): 2170, tỷ lệ 1.09, chi phí (%): 985, bộ đếm trục xuất nặng: 1, bộ nhớ đệm hiện tại DataBlock (%): 91 < bắt đầu
bị trục xuất (MB): 3763, tỷ lệ 1.08, chi phí (%): 1781, bộ đếm trục xuất nặng: 2, bộ nhớ đệm hiện tại DataBlock (%): 76
bị trục xuất (MB): 3306, tỷ lệ 1.07, chi phí (%): 1553, bộ đếm trục xuất nặng: 3, bộ nhớ đệm hiện tại DataBlock (%): 61
bị trục xuất (MB): 2508, tỷ lệ 1.06, chi phí (%): 1154, bộ đếm trục xuất nặng: 4, bộ nhớ đệm hiện tại DataBlock (%): 50
bị trục xuất (MB): 1824, tỷ lệ 1.04, chi phí (%): 812, bộ đếm trục xuất nặng: 5, bộ nhớ đệm hiện tại DataBlock (%): 42
bị trục xuất (MB): 1482, tỷ lệ 1.03, chi phí (%): 641, bộ đếm trục xuất nặng: 6, bộ nhớ đệm hiện tại DataBlock (%): 36
bị trục xuất (MB): 1140, tỷ lệ 1.01, chi phí (%): 470, bộ đếm trục xuất nặng: 7, bộ nhớ đệm hiện tại DataBlock (%): 32
bị trục xuất (MB): 913, tỷ lệ 1.0, chi phí (%): 356, bộ đếm trục xuất nặng: 8, bộ nhớ đệm hiện tại DataBlock (%): 29
bị trục xuất (MB): 912, tỷ lệ 0.89, chi phí (%): 356, bộ đếm trục xuất nặng: 9, bộ nhớ đệm hiện tại DataBlock (%): 26
bị trục xuất (MB): 684, tỷ lệ 0.76, chi phí (%): 242, bộ đếm trục xuất nặng: 10, bộ nhớ đệm hiện tại DataBlock (%): 24
bị trục xuất (MB): 684, tỷ lệ 0.61, chi phí (%): 242, bộ đếm trục xuất nặng: 11, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 456, tỷ lệ 0.51, chi phí (%): 128, bộ đếm trục xuất nặng: 12, bộ nhớ đệm hiện tại DataBlock (%): 21
bị trục xuất (MB): 456, tỷ lệ 0.42, chi phí (%): 128, bộ đếm trục xuất nặng: 13, bộ nhớ đệm hiện tại DataBlock (%): 20
bị trục xuất (MB): 456, tỷ lệ 0.33, chi phí (%): 128, bộ đếm trục xuất nặng: 14, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 15, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 342, tỷ lệ 0.32, chi phí (%): 71, bộ đếm trục xuất nặng: 16, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 342, tỷ lệ 0.31, chi phí (%): 71, bộ đếm trục xuất nặng: 17, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 228, tỷ lệ 0.3, chi phí (%): 14, bộ đếm trục xuất nặng: 18, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 228, tỷ lệ 0.29, chi phí (%): 14, bộ đếm trục xuất nặng: 19, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 228, tỷ lệ 0.27, chi phí (%): 14, bộ đếm trục xuất nặng: 20, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 228, tỷ lệ 0.25, chi phí (%): 14, bộ đếm trục xuất nặng: 21, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 228, tỷ lệ 0.24, chi phí (%): 14, bộ đếm trục xuất nặng: 22, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 228, tỷ lệ 0.22, chi phí (%): 14, bộ đếm trục xuất nặng: 23, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 228, tỷ lệ 0.21, chi phí (%): 14, bộ đếm trục xuất nặng: 24, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 228, tỷ lệ 0.2, chi phí (%): 14, bộ đếm trục xuất nặng: 25, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 228, tỷ lệ 0.17, chi phí (%): 14, bộ đếm trục xuất nặng: 26, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 456, tỷ lệ 0.17, chi phí chung (%): 128, bộ đếm trục xuất nặng: 27, bộ nhớ đệm hiện tại DataBlock (%): 18 < được thêm vào (nhưng bảng giống nhau)
bị trục xuất (MB): 456, tỷ lệ 0.15, chi phí (%): 128, bộ đếm trục xuất nặng: 28, bộ nhớ đệm hiện tại DataBlock (%): 17
bị trục xuất (MB): 342, tỷ lệ 0.13, chi phí (%): 71, bộ đếm trục xuất nặng: 29, bộ nhớ đệm hiện tại DataBlock (%): 17
bị trục xuất (MB): 342, tỷ lệ 0.11, chi phí (%): 71, bộ đếm trục xuất nặng: 30, bộ nhớ đệm hiện tại DataBlock (%): 17
bị trục xuất (MB): 342, tỷ lệ 0.09, chi phí (%): 71, bộ đếm trục xuất nặng: 31, bộ nhớ đệm hiện tại DataBlock (%): 17
bị trục xuất (MB): 228, tỷ lệ 0.08, chi phí (%): 14, bộ đếm trục xuất nặng: 32, bộ nhớ đệm hiện tại DataBlock (%): 17
bị trục xuất (MB): 228, tỷ lệ 0.07, chi phí (%): 14, bộ đếm trục xuất nặng: 33, bộ nhớ đệm hiện tại DataBlock (%): 17
bị trục xuất (MB): 228, tỷ lệ 0.06, chi phí (%): 14, bộ đếm trục xuất nặng: 34, bộ nhớ đệm hiện tại DataBlock (%): 17
bị trục xuất (MB): 228, tỷ lệ 0.05, chi phí (%): 14, bộ đếm trục xuất nặng: 35, bộ nhớ đệm hiện tại DataBlock (%): 17
bị trục xuất (MB): 228, tỷ lệ 0.05, chi phí (%): 14, bộ đếm trục xuất nặng: 36, bộ nhớ đệm hiện tại DataBlock (%): 17
bị trục xuất (MB): 228, tỷ lệ 0.04, chi phí (%): 14, bộ đếm trục xuất nặng: 37, bộ nhớ đệm hiện tại DataBlock (%): 17
bị trục xuất (MB): 109, tỷ lệ 0.04, chi phí hoạt động (%): -46, bộ đếm trục xuất nặng: 37, bộ nhớ đệm hiện tại DataBlock (%): 22 < áp suất ngược
bị trục xuất (MB): 798, tỷ lệ 0.24, chi phí (%): 299, bộ đếm trục xuất nặng: 38, bộ nhớ đệm hiện tại DataBlock (%): 20
bị trục xuất (MB): 798, tỷ lệ 0.29, chi phí (%): 299, bộ đếm trục xuất nặng: 39, bộ nhớ đệm hiện tại DataBlock (%): 18
bị trục xuất (MB): 570, tỷ lệ 0.27, chi phí (%): 185, bộ đếm trục xuất nặng: 40, bộ nhớ đệm hiện tại DataBlock (%): 17
bị trục xuất (MB): 456, tỷ lệ 0.22, chi phí (%): 128, bộ đếm trục xuất nặng: 41, bộ nhớ đệm hiện tại DataBlock (%): 16
bị trục xuất (MB): 342, tỷ lệ 0.16, chi phí (%): 71, bộ đếm trục xuất nặng: 42, bộ nhớ đệm hiện tại DataBlock (%): 16
bị trục xuất (MB): 342, tỷ lệ 0.11, chi phí (%): 71, bộ đếm trục xuất nặng: 43, bộ nhớ đệm hiện tại DataBlock (%): 16
bị trục xuất (MB): 228, tỷ lệ 0.09, chi phí (%): 14, bộ đếm trục xuất nặng: 44, bộ nhớ đệm hiện tại DataBlock (%): 16
bị trục xuất (MB): 228, tỷ lệ 0.07, chi phí (%): 14, bộ đếm trục xuất nặng: 45, bộ nhớ đệm hiện tại DataBlock (%): 16
bị trục xuất (MB): 228, tỷ lệ 0.05, chi phí (%): 14, bộ đếm trục xuất nặng: 46, bộ nhớ đệm hiện tại DataBlock (%): 16
bị trục xuất (MB): 222, tỷ lệ 0.04, chi phí (%): 11, bộ đếm trục xuất nặng: 47, bộ nhớ đệm hiện tại DataBlock (%): 16
bị trục xuất (MB): 104, tỷ lệ 0.03, chi phí (%): -48, bộ đếm trục xuất nặng: 47, bộ nhớ đệm hiện tại DataBlock (%): 21 < bị gián đoạn
bị trục xuất (MB): 684, tỷ lệ 0.2, chi phí (%): 242, bộ đếm trục xuất nặng: 48, bộ nhớ đệm hiện tại DataBlock (%): 19
bị trục xuất (MB): 570, tỷ lệ 0.23, chi phí (%): 185, bộ đếm trục xuất nặng: 49, bộ nhớ đệm hiện tại DataBlock (%): 18
bị trục xuất (MB): 342, tỷ lệ 0.22, chi phí (%): 71, bộ đếm trục xuất nặng: 50, bộ nhớ đệm hiện tại DataBlock (%): 18
bị trục xuất (MB): 228, tỷ lệ 0.21, chi phí (%): 14, bộ đếm trục xuất nặng: 51, bộ nhớ đệm hiện tại DataBlock (%): 18
bị trục xuất (MB): 228, tỷ lệ 0.2, chi phí (%): 14, bộ đếm trục xuất nặng: 52, bộ nhớ đệm hiện tại DataBlock (%): 18
bị trục xuất (MB): 228, tỷ lệ 0.18, chi phí (%): 14, bộ đếm trục xuất nặng: 53, bộ nhớ đệm hiện tại DataBlock (%): 18
bị trục xuất (MB): 228, tỷ lệ 0.16, chi phí (%): 14, bộ đếm trục xuất nặng: 54, bộ nhớ đệm hiện tại DataBlock (%): 18
bị trục xuất (MB): 228, tỷ lệ 0.14, chi phí (%): 14, bộ đếm trục xuất nặng: 55, bộ nhớ đệm hiện tại DataBlock (%): 18
bị trục xuất (MB): 112, tỷ lệ 0.14, chi phí hoạt động (%): -44, bộ đếm trục xuất nặng: 55, bộ nhớ đệm hiện tại DataBlock (%): 23 < áp suất ngược
bị trục xuất (MB): 456, tỷ lệ 0.26, chi phí (%): 128, bộ đếm trục xuất nặng: 56, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.31, chi phí (%): 71, bộ đếm trục xuất nặng: 57, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 58, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 59, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 60, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 61, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 62, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 63, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.32, chi phí (%): 71, bộ đếm trục xuất nặng: 64, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 65, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 66, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.32, chi phí (%): 71, bộ đếm trục xuất nặng: 67, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 68, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.32, chi phí (%): 71, bộ đếm trục xuất nặng: 69, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.32, chi phí (%): 71, bộ đếm trục xuất nặng: 70, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 71, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 72, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 73, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 74, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 75, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 342, tỷ lệ 0.33, chi phí (%): 71, bộ đếm trục xuất nặng: 76, bộ nhớ đệm hiện tại DataBlock (%): 22
bị trục xuất (MB): 21, tỷ lệ 0.33, chi phí (%): -90, bộ đếm trục xuất nặng: 76, bộ nhớ đệm hiện tại DataBlock (%): 32
bị trục xuất (MB): 0, tỷ lệ 0.0, chi phí (%): -100, bộ đếm trục xuất nặng: 0, bộ nhớ đệm hiện tại DataBlock (%): 100
bị trục xuất (MB): 0, tỷ lệ 0.0, chi phí (%): -100, bộ đếm trục xuất nặng: 0, bộ nhớ đệm hiện tại DataBlock (%): 100

Cần phải quét để hiển thị cùng một quy trình dưới dạng biểu đồ về mối quan hệ giữa hai phần bộ đệm - đơn (trong đó các khối chưa từng được yêu cầu trước đó) và đa (dữ liệu “được yêu cầu” ít nhất một lần được lưu trữ tại đây):

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Và cuối cùng, hoạt động của các tham số ở dạng biểu đồ trông như thế nào. Để so sánh, bộ đệm đã bị tắt hoàn toàn ngay từ đầu, sau đó HBase được khởi chạy với bộ đệm và trì hoãn việc bắt đầu công việc tối ưu hóa trong 5 phút (30 chu kỳ loại bỏ).

Mã đầy đủ có thể được tìm thấy trong Yêu cầu kéo HBASE 23887 trên github.

Tuy nhiên, 300 nghìn lượt đọc mỗi giây không phải là tất cả những gì có thể đạt được trên phần cứng này trong những điều kiện này. Thực tế là khi bạn cần truy cập dữ liệu qua HDFS, cơ chế ShortCircuitCache (sau đây gọi là SSC) sẽ được sử dụng, cho phép bạn truy cập dữ liệu trực tiếp, tránh tương tác mạng.

Hồ sơ cho thấy mặc dù cơ chế này mang lại lợi ích lớn nhưng đôi khi nó cũng trở thành nút thắt cổ chai, bởi vì hầu hết tất cả các hoạt động nặng đều diễn ra bên trong một khóa, dẫn đến tình trạng khóa hầu hết thời gian.

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Sau khi nhận ra điều này, chúng tôi nhận ra rằng vấn đề có thể được khắc phục bằng cách tạo ra một loạt SSC độc lập:

private final ShortCircuitCache[] shortCircuitCache;
...
shortCircuitCache = new ShortCircuitCache[this.clientShortCircuitNum];
for (int i = 0; i < this.clientShortCircuitNum; i++)
  this.shortCircuitCache[i] = new ShortCircuitCache(…);

Và sau đó làm việc với chúng, loại trừ các giao điểm ở chữ số offset cuối cùng:

public ShortCircuitCache getShortCircuitCache(long idx) {
    return shortCircuitCache[(int) (idx % clientShortCircuitNum)];
}

Bây giờ bạn có thể bắt đầu thử nghiệm. Để thực hiện việc này, chúng ta sẽ đọc các tệp từ HDFS bằng một ứng dụng đa luồng đơn giản. Đặt các thông số:

conf.set("dfs.client.read.shortcircuit", "true");
conf.set("dfs.client.read.shortcircuit.buffer.size", "65536"); // по дефолту = 1 МБ и это сильно замедляет чтение, поэтому лучше привести в соответствие к реальным нуждам
conf.set("dfs.client.short.circuit.num", num); // от 1 до 10

Và chỉ cần đọc các tập tin:

FSDataInputStream in = fileSystem.open(path);
for (int i = 0; i < count; i++) {
    position += 65536;
    if (position > 900000000)
        position = 0L;
    int res = in.read(position, byteBuffer, 0, 65536);
}

Mã này được thực thi trong các luồng riêng biệt và chúng tôi sẽ tăng số lượng tệp đọc đồng thời (từ 10 lên 200 - trục hoành) và số lượng bộ đệm (từ 1 lên 10 - đồ họa). Trục tung biểu thị khả năng tăng tốc do tăng SSC so với trường hợp chỉ có một bộ nhớ đệm.

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Cách đọc biểu đồ: Thời gian thực hiện 100 nghìn lượt đọc trong khối 64 KB với một bộ đệm cần 78 giây. Trong khi với 5 bộ đệm thì mất 16 giây. Những thứ kia. có gia tốc ~ 5 lần. Như có thể thấy từ biểu đồ, hiệu ứng này không đáng chú ý lắm đối với một số lượng nhỏ các lần đọc song song, nó bắt đầu đóng một vai trò đáng chú ý khi có hơn 50 lượt đọc luồng. Điều đáng chú ý là việc tăng số lượng SSC từ 6 trở lên mang lại mức tăng hiệu suất nhỏ hơn đáng kể.

Lưu ý 1: vì kết quả kiểm tra khá biến động (xem bên dưới), nên đã thực hiện 3 lần chạy và giá trị kết quả được tính trung bình.

Lưu ý 2: Hiệu suất đạt được từ việc định cấu hình truy cập ngẫu nhiên là như nhau, mặc dù bản thân quyền truy cập chậm hơn một chút.

Tuy nhiên, cần phải làm rõ rằng, không giống như trường hợp của HBase, khả năng tăng tốc này không phải lúc nào cũng miễn phí. Ở đây, chúng tôi “mở khóa” khả năng của CPU để làm việc nhiều hơn thay vì bị treo.

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Ở đây bạn có thể quan sát thấy rằng, nói chung, việc tăng số lượng bộ nhớ đệm sẽ làm tăng mức sử dụng CPU theo tỷ lệ gần đúng. Tuy nhiên, có nhiều sự kết hợp chiến thắng hơn một chút.

Ví dụ: chúng ta hãy xem xét kỹ hơn cài đặt SSC = 3. Mức tăng hiệu suất trên phạm vi là khoảng 3.3 lần. Dưới đây là kết quả của cả ba lần chạy riêng biệt.

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Trong khi mức tiêu thụ CPU tăng khoảng 2.8 lần. Sự khác biệt không lớn lắm nhưng cô bé Greta đã rất vui và có thể có thời gian đến trường và học bài.

Do đó, điều này sẽ có tác động tích cực đối với bất kỳ công cụ nào sử dụng quyền truy cập hàng loạt vào HDFS (ví dụ Spark, v.v.), miễn là mã ứng dụng nhẹ (tức là phích cắm nằm ở phía máy khách HDFS) và có nguồn CPU miễn phí . Để kiểm tra, hãy kiểm tra xem việc sử dụng kết hợp tối ưu hóa BlockCache và điều chỉnh SSC để đọc từ HBase sẽ có tác dụng gì.

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Có thể thấy, trong điều kiện như vậy, hiệu quả không lớn như trong các bài kiểm tra tinh chỉnh (đọc mà không cần xử lý), nhưng ở đây hoàn toàn có thể ép ra thêm 80K. Cùng với nhau, cả hai sự tối ưu hóa đều mang lại tốc độ tăng gấp 4 lần.

Một PR cũng đã được thực hiện cho việc tối ưu hóa này [HDFS-15202], đã được hợp nhất và chức năng này sẽ có trong các phiên bản tương lai.

Và cuối cùng, thật thú vị khi so sánh hiệu suất đọc của cơ sở dữ liệu cột rộng tương tự, Cassandra và HBase.

Để thực hiện điều này, chúng tôi đã khởi chạy các phiên bản của tiện ích kiểm tra tải YCSB tiêu chuẩn từ hai máy chủ (tổng cộng 800 luồng). Về phía máy chủ - 4 phiên bản của RegionServer và Cassandra trên 4 máy chủ (không phải các phiên bản mà máy khách đang chạy, để tránh ảnh hưởng của chúng). Bài đọc đến từ các bảng có kích thước:

HBase - 300 GB trên HDFS (dữ liệu thuần 100 GB)

Cassandra - 250 GB (hệ số sao chép = 3)

Những thứ kia. âm lượng gần như nhau (trong HBase nhiều hơn một chút).

Thông số HBase:

dfs.client.short. Circuit.num = 5 (Tối ưu hóa máy khách HDFS)

hbase.lru.cache.heavy.eviction.count.limit = 30 - điều này có nghĩa là bản vá sẽ bắt đầu hoạt động sau 30 lần trục xuất (~5 phút)

hbase.lru.cache.heavy.eviction.mb.size.limit = 300 — khối lượng mục tiêu của bộ nhớ đệm và trục xuất

Nhật ký YCSB đã được phân tích cú pháp và biên dịch thành biểu đồ Excel:

Cách tăng tốc độ đọc từ HBase lên 3 lần và từ HDFS lên 5 lần

Như bạn có thể thấy, những tối ưu hóa này giúp bạn có thể so sánh hiệu suất của các cơ sở dữ liệu này trong những điều kiện này và đạt được 450 nghìn lượt đọc mỗi giây.

Chúng tôi hy vọng thông tin này có thể hữu ích cho ai đó trong cuộc đấu tranh thú vị về năng suất.

Nguồn: www.habr.com

Thêm một lời nhận xét