Cách thức và lý do chúng tôi viết dịch vụ có khả năng mở rộng tải cao cho 1C: Enterprise: Java, PostgreSQL, Hazelcast

Trong bài viết này, chúng tôi sẽ nói về cách thức và lý do chúng tôi phát triển Hệ thống tương tác – một cơ chế truyền thông tin giữa các ứng dụng khách và máy chủ 1C:Enterprise - từ việc thiết lập tác vụ đến suy nghĩ về kiến ​​trúc và chi tiết triển khai.

Hệ thống tương tác (sau đây gọi là SV) là một hệ thống nhắn tin phân tán, có khả năng chịu lỗi với khả năng gửi được đảm bảo. SV được thiết kế như một dịch vụ tải cao với khả năng mở rộng cao, có sẵn dưới dạng dịch vụ trực tuyến (do 1C cung cấp) và dưới dạng sản phẩm được sản xuất hàng loạt có thể được triển khai trên cơ sở máy chủ của riêng bạn.

SV sử dụng bộ nhớ phân tán màu hạt dẻ và công cụ tìm kiếm Elasticsearch. Chúng ta cũng sẽ nói về Java và cách chúng tôi mở rộng quy mô PostgreSQL theo chiều ngang.
Cách thức và lý do chúng tôi viết dịch vụ có khả năng mở rộng tải cao cho 1C: Enterprise: Java, PostgreSQL, Hazelcast

Báo cáo sự cố

Để làm rõ lý do tại sao chúng tôi tạo ra Hệ thống tương tác, tôi sẽ cho bạn biết một chút về cách hoạt động của quá trình phát triển các ứng dụng kinh doanh trong 1C.

Để bắt đầu, hãy giới thiệu một chút về chúng tôi cho những ai chưa biết chúng tôi làm gì :) Chúng tôi đang tạo ra nền tảng công nghệ 1C:Enterprise. Nền tảng này bao gồm một công cụ phát triển ứng dụng kinh doanh cũng như thời gian chạy cho phép các ứng dụng kinh doanh chạy trong môi trường đa nền tảng.

Mô hình phát triển client-server

Các ứng dụng kinh doanh được tạo trên 1C:Enterprise hoạt động theo ba cấp độ máy khách-máy chủ kiến trúc “DBMS – máy chủ ứng dụng – máy khách”. Mã ứng dụng được viết bằng ngôn ngữ 1C tích hợp, có thể được thực thi trên máy chủ ứng dụng hoặc trên máy khách. Tất cả công việc với các đối tượng ứng dụng (thư mục, tài liệu, v.v.), cũng như việc đọc và ghi cơ sở dữ liệu, chỉ được thực hiện trên máy chủ. Chức năng của biểu mẫu và giao diện lệnh cũng được triển khai trên máy chủ. Khách hàng thực hiện nhận, mở và hiển thị các biểu mẫu, “giao tiếp” với người dùng (cảnh báo, câu hỏi…), các phép tính nhỏ dưới dạng biểu mẫu yêu cầu phản hồi nhanh (ví dụ: nhân giá theo số lượng), làm việc với các tệp cục bộ, làm việc với thiết bị.

Trong mã ứng dụng, tiêu đề của thủ tục và hàm phải chỉ rõ nơi mã sẽ được thực thi - sử dụng chỉ thị &AtClient / &AtServer (&AtClient / &AtServer trong phiên bản tiếng Anh của ngôn ngữ). Các nhà phát triển 1C bây giờ sẽ sửa lỗi cho tôi bằng cách nói rằng các chỉ thị thực sự là nhiều hơn, nhưng đối với chúng tôi điều này bây giờ không còn quan trọng nữa.

Bạn có thể gọi mã máy chủ từ mã máy khách, nhưng bạn không thể gọi mã máy khách từ mã máy chủ. Đây là một hạn chế cơ bản mà chúng tôi đã thực hiện vì một số lý do. Đặc biệt, bởi vì mã máy chủ phải được viết theo cách mà nó thực thi theo cùng một cách bất kể nó được gọi ở đâu - từ máy khách hay từ máy chủ. Và trong trường hợp gọi mã máy chủ từ mã máy chủ khác thì không có máy khách nào như vậy. Và bởi vì trong quá trình thực thi mã máy chủ, máy khách gọi nó có thể đóng, thoát khỏi ứng dụng và máy chủ sẽ không còn ai để gọi.

Cách thức và lý do chúng tôi viết dịch vụ có khả năng mở rộng tải cao cho 1C: Enterprise: Java, PostgreSQL, Hazelcast
Mã xử lý thao tác bấm nút: gọi thủ tục máy chủ từ máy khách sẽ hoạt động, gọi thủ tục máy khách từ máy chủ sẽ không hoạt động

Điều này có nghĩa là nếu chúng tôi muốn gửi một số thông báo từ máy chủ đến ứng dụng khách, chẳng hạn như việc tạo báo cáo "chạy dài" đã kết thúc và có thể xem báo cáo, thì chúng tôi không có phương pháp như vậy. Bạn phải sử dụng các thủ thuật, chẳng hạn như thăm dò định kỳ máy chủ từ mã máy khách. Nhưng cách tiếp cận này tải vào hệ thống những lệnh gọi không cần thiết và nhìn chung trông không được trang nhã cho lắm.

Và cũng có một nhu cầu, chẳng hạn, khi có một cuộc gọi điện thoại đến SIP- khi thực hiện cuộc gọi, hãy thông báo cho ứng dụng khách về điều này để ứng dụng có thể sử dụng số của người gọi để tìm số đó trong cơ sở dữ liệu đối tác và hiển thị thông tin người dùng về đối tác đang gọi. Hoặc, ví dụ, khi một đơn hàng đến kho, hãy thông báo cho ứng dụng khách của khách hàng về điều này. Nói chung, có nhiều trường hợp cơ chế như vậy sẽ hữu ích.

Bản thân việc sản xuất

Tạo cơ chế nhắn tin. Nhanh chóng, đáng tin cậy, giao hàng đảm bảo, có khả năng tìm kiếm tin nhắn linh hoạt. Dựa trên cơ chế, triển khai một trình nhắn tin (tin nhắn, cuộc gọi video) chạy bên trong ứng dụng 1C.

Thiết kế hệ thống có khả năng mở rộng theo chiều ngang. Tải ngày càng tăng phải được giải quyết bằng cách tăng số lượng nút.

Thực hiện

Chúng tôi quyết định không tích hợp trực tiếp phần máy chủ của SV vào nền tảng 1C:Enterprise mà triển khai nó dưới dạng một sản phẩm riêng biệt, API của sản phẩm này có thể được gọi từ mã của các giải pháp ứng dụng 1C. Điều này được thực hiện vì một số lý do, trong đó lý do chính là tôi muốn có thể trao đổi tin nhắn giữa các ứng dụng 1C khác nhau (ví dụ: giữa Quản lý Thương mại và Kế toán). Các ứng dụng 1C khác nhau có thể chạy trên các phiên bản khác nhau của nền tảng 1C:Enterprise, được đặt trên các máy chủ khác nhau, v.v. Trong điều kiện như vậy, việc triển khai SV như một sản phẩm riêng biệt nằm “bên cạnh” cài đặt 1C là giải pháp tối ưu.

Vì vậy, chúng tôi quyết định tạo SV thành một sản phẩm riêng biệt. Chúng tôi khuyên các công ty nhỏ nên sử dụng máy chủ CB mà chúng tôi đã cài đặt trên đám mây của mình (wss://1cdialog.com) để tránh các chi phí chung liên quan đến việc cài đặt và cấu hình cục bộ của máy chủ. Các khách hàng lớn có thể thấy nên cài đặt máy chủ CB của riêng họ tại cơ sở của họ. Chúng tôi đã sử dụng cách tiếp cận tương tự trong sản phẩm SaaS trên nền tảng đám mây của mình 1cTươi – nó được sản xuất dưới dạng sản phẩm sản xuất hàng loạt để cài đặt tại địa điểm của khách hàng và cũng được triển khai trên đám mây của chúng tôi https://1cfresh.com/.

Ứng dụng

Để phân phối tải và khả năng chịu lỗi, chúng tôi sẽ triển khai không chỉ một ứng dụng Java mà nhiều ứng dụng với bộ cân bằng tải phía trước chúng. Nếu bạn cần chuyển tin nhắn từ nút này sang nút khác, hãy sử dụng xuất bản/đăng ký trong Hazelcast.

Giao tiếp giữa máy khách và máy chủ được thực hiện thông qua websocket. Nó rất phù hợp cho các hệ thống thời gian thực.

Bộ đệm phân phối

Chúng tôi đã chọn giữa Redis, Hazelcast và Ehcache. Đó là năm 2015. Redis vừa ra cluster mới (mới quá, đáng sợ), có Sentinel với rất nhiều hạn chế. Ehcache không biết cách tập hợp thành một cụm (chức năng này xuất hiện sau). Chúng tôi quyết định dùng thử với Hazelcast 3.4.
Hazelcast được tập hợp thành một cụm ngay lập tức. Ở chế độ một nút, nó không hữu ích lắm và chỉ có thể được sử dụng làm bộ đệm - nó không biết cách đổ dữ liệu vào đĩa, nếu mất nút duy nhất, bạn sẽ mất dữ liệu. Chúng tôi triển khai một số Hazelcast, trong đó chúng tôi sao lưu dữ liệu quan trọng. Chúng tôi không sao lưu bộ nhớ đệm – chúng tôi không bận tâm về điều đó.

Đối với chúng tôi, Hazelcast là:

  • Lưu trữ phiên người dùng. Mỗi lần truy cập cơ sở dữ liệu cho một phiên sẽ mất nhiều thời gian, vì vậy chúng tôi đã đưa tất cả các phiên vào Hazelcast.
  • Bộ nhớ đệm. Nếu bạn đang tìm kiếm hồ sơ người dùng, hãy kiểm tra bộ đệm. Đã viết một tin nhắn mới - đặt nó vào bộ nhớ đệm.
  • Các chủ đề để giao tiếp giữa các phiên bản ứng dụng. Nút tạo ra một sự kiện và đặt nó vào chủ đề Hazelcast. Các nút ứng dụng khác đã đăng ký chủ đề này sẽ nhận và xử lý sự kiện.
  • Khóa cụm. Ví dụ: chúng tôi tạo một cuộc thảo luận bằng một khóa duy nhất (thảo luận đơn lẻ trong cơ sở dữ liệu 1C):

conversationKeyChecker.check("БЕНЗОКОЛОНКА");

      doInClusterLock("БЕНЗОКОЛОНКА", () -> {

          conversationKeyChecker.check("БЕНЗОКОЛОНКА");

          createChannel("БЕНЗОКОЛОНКА");
      });

Chúng tôi đã kiểm tra rằng không có kênh. Chúng tôi đã lấy khóa, kiểm tra lại và tạo nó. Nếu bạn không kiểm tra khóa sau khi lấy khóa thì có khả năng một chuỗi khác cũng đã kiểm tra vào thời điểm đó và bây giờ sẽ cố gắng tạo cuộc thảo luận tương tự - nhưng nó đã tồn tại. Bạn không thể khóa bằng cách sử dụng Khóa java được đồng bộ hóa hoặc thông thường. Thông qua cơ sở dữ liệu - nó chậm và thật đáng tiếc cho cơ sở dữ liệu; thông qua Hazelcast - đó là những gì bạn cần.

Chọn một DBMS

Chúng tôi có nhiều kinh nghiệm và thành công khi làm việc với PostgreSQL và cộng tác với các nhà phát triển DBMS này.

Thật không dễ dàng với cụm PostgreSQL - có XL, XC, thành phố, nhưng nói chung đây không phải là những NoSQL có thể mở rộng quy mô ngay lập tức. Chúng tôi không coi NoSQL là kho lưu trữ chính; chỉ cần sử dụng Hazelcast, công cụ mà trước đây chúng tôi chưa từng làm việc với là đủ.

Nếu bạn cần mở rộng quy mô cơ sở dữ liệu quan hệ, điều đó có nghĩa là mảnh vỡ. Như bạn đã biết, với sharding chúng ta chia cơ sở dữ liệu thành các phần riêng biệt để mỗi phần có thể được đặt trên một máy chủ riêng biệt.

Phiên bản đầu tiên của shending của chúng tôi giả định khả năng phân phối từng bảng trong ứng dụng của chúng tôi trên các máy chủ khác nhau theo các tỷ lệ khác nhau. Có rất nhiều thông báo trên máy chủ A - làm ơn, hãy chuyển một phần của bảng này sang máy chủ B. Quyết định này chỉ đơn giản là hét lên về việc tối ưu hóa quá sớm, vì vậy chúng tôi quyết định giới hạn bản thân trong cách tiếp cận nhiều người thuê.

Bạn có thể đọc về nhiều người thuê nhà, ví dụ, trên trang web Dữ liệu Citus.

SV có các khái niệm về ứng dụng và người đăng ký. Ứng dụng là một bản cài đặt cụ thể của một ứng dụng kinh doanh, chẳng hạn như ERP hoặc Kế toán, với người dùng và dữ liệu kinh doanh của ứng dụng đó. Người đăng ký là tổ chức hoặc cá nhân thay mặt họ đăng ký ứng dụng trên máy chủ SV. Một thuê bao có thể đăng ký nhiều ứng dụng và các ứng dụng này có thể trao đổi tin nhắn với nhau. Người đăng ký đã trở thành người thuê trong hệ thống của chúng tôi. Tin nhắn từ nhiều người đăng ký có thể được lưu trữ trong một cơ sở dữ liệu vật lý; nếu chúng tôi thấy rằng một người đăng ký đã bắt đầu tạo ra nhiều lưu lượng truy cập, chúng tôi sẽ chuyển nó sang một cơ sở dữ liệu vật lý riêng biệt (hoặc thậm chí là một máy chủ cơ sở dữ liệu riêng biệt).

Chúng tôi có cơ sở dữ liệu chính nơi bảng định tuyến được lưu trữ với thông tin về vị trí của tất cả cơ sở dữ liệu người đăng ký.

Cách thức và lý do chúng tôi viết dịch vụ có khả năng mở rộng tải cao cho 1C: Enterprise: Java, PostgreSQL, Hazelcast

Để tránh tình trạng tắc nghẽn cơ sở dữ liệu chính, chúng tôi giữ bảng định tuyến (và các dữ liệu thường xuyên cần thiết khác) trong bộ nhớ đệm.

Nếu cơ sở dữ liệu của người đăng ký bắt đầu chậm lại, chúng tôi sẽ cắt nó thành các phân vùng bên trong. Trên các dự án khác chúng tôi sử dụng pg_pathman.

Vì việc mất tin nhắn của người dùng là điều không tốt nên chúng tôi duy trì cơ sở dữ liệu của mình bằng các bản sao. Sự kết hợp giữa các bản sao đồng bộ và không đồng bộ cho phép bạn tự bảo hiểm trong trường hợp mất cơ sở dữ liệu chính. Việc mất tin nhắn sẽ chỉ xảy ra nếu cơ sở dữ liệu chính và bản sao đồng bộ của nó bị lỗi đồng thời.

Nếu bản sao đồng bộ bị mất, bản sao không đồng bộ sẽ trở thành bản sao đồng bộ.
Nếu cơ sở dữ liệu chính bị mất, bản sao đồng bộ sẽ trở thành cơ sở dữ liệu chính và bản sao không đồng bộ sẽ trở thành bản sao đồng bộ.

Elaticsearch cho tìm kiếm

Vì, ngoài những thứ khác, SV cũng là một trình nhắn tin nên nó yêu cầu tìm kiếm nhanh chóng, thuận tiện và linh hoạt, có tính đến hình thái học, sử dụng các kết quả khớp không chính xác. Chúng tôi quyết định không phát minh lại bánh xe và sử dụng công cụ tìm kiếm miễn phí Elaticsearch, được tạo dựa trên thư viện Lucene. Chúng tôi cũng triển khai Elaticsearch trong một cụm (chính – dữ liệu – dữ liệu) để loại bỏ các vấn đề trong trường hợp các nút ứng dụng bị lỗi.

Trên github chúng tôi đã tìm thấy Plugin hình thái học tiếng Nga cho Elaticsearch và sử dụng nó. Trong chỉ mục Elaticsearch, chúng tôi lưu trữ gốc từ (mà plugin xác định) và N-gram. Khi người dùng nhập văn bản để tìm kiếm, chúng tôi sẽ tìm văn bản đã nhập trong số N-gram. Khi được lưu vào chỉ mục, từ “văn bản” sẽ được chia thành các N-gram sau:

[những, tek, tex, văn bản, văn bản, ek, ex, ext, văn bản, ks, kst, ksty, st, sty, bạn],

Và gốc của từ “văn bản” cũng sẽ được giữ nguyên. Cách tiếp cận này cho phép bạn tìm kiếm ở đầu, ở giữa và ở cuối từ.

Bức tranh lớn

Cách thức và lý do chúng tôi viết dịch vụ có khả năng mở rộng tải cao cho 1C: Enterprise: Java, PostgreSQL, Hazelcast
Lặp lại hình ảnh từ đầu bài nhưng có giải thích:

  • Balancer xuất hiện trên Internet; chúng tôi có nginx, nó có thể là bất kỳ.
  • Các phiên bản ứng dụng Java giao tiếp với nhau thông qua Hazelcast.
  • Để làm việc với một ổ cắm web, chúng tôi sử dụng Netty.
  • Ứng dụng Java được viết bằng Java 8 và bao gồm các gói hệ điều hành. Các kế hoạch bao gồm việc di chuyển sang Java 10 và chuyển đổi sang các mô-đun.

Phát triển và thử nghiệm

Trong quá trình phát triển và thử nghiệm SV, chúng tôi đã tìm thấy một số tính năng thú vị của sản phẩm chúng tôi sử dụng.

Kiểm tra tải và rò rỉ bộ nhớ

Việc phát hành mỗi bản phát hành SV đều liên quan đến việc kiểm tra tải. Thành công khi:

  • Quá trình thử nghiệm đã diễn ra trong vài ngày và không có lỗi dịch vụ nào
  • Thời gian phản hồi các thao tác phím không vượt quá ngưỡng thoải mái
  • Hiệu suất suy giảm so với phiên bản trước không quá 10%

Chúng tôi điền dữ liệu vào cơ sở dữ liệu thử nghiệm - để thực hiện việc này, chúng tôi nhận được thông tin về người đăng ký tích cực nhất từ ​​máy chủ sản xuất, nhân số của nó với 5 (số lượng tin nhắn, cuộc thảo luận, người dùng) và kiểm tra theo cách đó.

Chúng tôi thực hiện kiểm tra tải hệ thống tương tác theo ba cấu hình:

  1. kiểm tra căng thẳng
  2. Chỉ kết nối
  3. Đăng ký thuê bao

Trong quá trình kiểm tra căng thẳng, chúng tôi khởi chạy hàng trăm luồng và chúng tải hệ thống không ngừng: viết tin nhắn, tạo cuộc thảo luận, nhận danh sách tin nhắn. Chúng tôi mô phỏng hành động của người dùng thông thường (lấy danh sách tin nhắn chưa đọc của tôi, viết thư cho ai đó) và các giải pháp phần mềm (truyền gói có cấu hình khác, xử lý cảnh báo).

Ví dụ: đây là phần của bài kiểm tra căng thẳng:

  • Người dùng đăng nhập
    • Yêu cầu các cuộc thảo luận chưa đọc của bạn
    • 50% có khả năng đọc tin nhắn
    • 50% có khả năng nhắn tin
    • Người dùng tiếp theo:
      • Có 20% cơ hội tạo một cuộc thảo luận mới
      • Chọn ngẫu nhiên bất kỳ cuộc thảo luận nào của nó
      • Đi vào trong
      • Yêu cầu tin nhắn, hồ sơ người dùng
      • Tạo năm tin nhắn gửi đến người dùng ngẫu nhiên từ cuộc thảo luận này
      • Rời khỏi cuộc thảo luận
      • Lặp lại 20 lần
      • Đăng xuất, quay lại phần đầu của tập lệnh

    • Chatbot vào hệ thống (mô phỏng nhắn tin từ mã ứng dụng)
      • Có 50% cơ hội tạo kênh mới để trao đổi dữ liệu (thảo luận đặc biệt)
      • 50% có khả năng viết tin nhắn cho bất kỳ kênh hiện có nào

Kịch bản “Chỉ kết nối” xuất hiện là có lý do. Có một tình trạng là người dùng đã kết nối vào hệ thống nhưng chưa tham gia. Mỗi người dùng bật máy tính lúc 09:00 sáng, thiết lập kết nối với máy chủ và giữ im lặng. Những kẻ này rất nguy hiểm, có rất nhiều người trong số họ - gói duy nhất họ có là PING/PONG, nhưng họ vẫn giữ kết nối với máy chủ (họ không thể duy trì được - nếu có tin nhắn mới thì sao). Thử nghiệm tái hiện tình huống trong đó một số lượng lớn người dùng như vậy cố gắng đăng nhập vào hệ thống trong nửa giờ. Nó tương tự như một bài kiểm tra căng thẳng, nhưng trọng tâm của nó chính xác là ở đầu vào đầu tiên này - để không xảy ra lỗi (một người không sử dụng hệ thống và nó đã rơi ra - rất khó để nghĩ ra điều gì đó tồi tệ hơn).

Kịch bản đăng ký thuê bao bắt đầu từ lần khởi chạy đầu tiên. Chúng tôi đã tiến hành một bài kiểm tra căng thẳng và chắc chắn rằng hệ thống không bị chậm lại trong quá trình trao đổi thư từ. Nhưng người dùng đã đến và việc đăng ký bắt đầu thất bại do hết thời gian chờ. Khi đăng ký chúng tôi đã sử dụng / dev / random, liên quan đến entropy của hệ thống. Máy chủ không có thời gian để tích lũy đủ entropy và khi yêu cầu SecureRandom mới, nó bị treo trong hàng chục giây. Có nhiều cách để thoát khỏi tình huống này, ví dụ: chuyển sang /dev/urandom kém an toàn hơn, cài đặt một bảng đặc biệt tạo ra entropy, tạo trước các số ngẫu nhiên và lưu trữ chúng trong một nhóm. Chúng tôi đã tạm thời khắc phục sự cố với nhóm, nhưng kể từ đó, chúng tôi đã chạy thử nghiệm riêng để đăng ký người đăng ký mới.

Chúng tôi sử dụng như một máy phát điện tải JMeter. Nó không biết cách làm việc với websocket; nó cần một plugin. Kết quả đầu tiên trong tìm kiếm cho truy vấn “jmeter websocket” là: bài viết từ BlazeMeter, đề nghị plugin của Maciej Zaleski.

Đó là nơi chúng tôi quyết định bắt đầu.

Gần như ngay lập tức sau khi bắt đầu thử nghiệm nghiêm túc, chúng tôi phát hiện ra rằng JMeter bắt đầu rò rỉ bộ nhớ.

Plugin này là một câu chuyện lớn riêng biệt; với 176 sao, nó có 132 nhánh trên github. Bản thân tác giả đã không cam kết với nó kể từ năm 2015 (chúng tôi đã thực hiện nó vào năm 2015, sau đó nó không gây nghi ngờ), một số vấn đề về github liên quan đến rò rỉ bộ nhớ, 7 yêu cầu kéo không được tiết lộ.
Nếu bạn quyết định thực hiện kiểm tra tải bằng plugin này, vui lòng chú ý đến các cuộc thảo luận sau:

  1. Trong môi trường đa luồng, LinkedList thông thường đã được sử dụng và kết quả là NPE trong thời gian chạy. Điều này có thể được giải quyết bằng cách chuyển sang ConcurrentLinkedDeque hoặc bằng các khối được đồng bộ hóa. Chúng tôi đã chọn phương án đầu tiên cho mình (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/43).
  2. Rò rỉ bộ nhớ; khi ngắt kết nối, thông tin kết nối không bị xóa (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/44).
  3. Ở chế độ phát trực tuyến (khi websocket không được đóng ở cuối mẫu nhưng được sử dụng sau trong kế hoạch), các mẫu Phản hồi không hoạt động (https://github.com/maciejzaleski/JMeter-WebSocketSampler/issues/19).

Đây là một trong những cái đó trên github. Những gì chúng ta đã làm:

  1. Đã lấy cái nĩa Elyran Kogan (@elyrank) – nó khắc phục vấn đề 1 và 3
  2. Đã giải quyết vấn đề 2
  3. Cập nhật cầu cảng từ 9.2.14 lên 9.3.12
  4. Đã gói SimpleDateFormat trong ThreadLocal; SimpleDateFormat không an toàn theo luồng, dẫn đến NPE khi chạy
  5. Đã sửa lỗi rò rỉ bộ nhớ khác (kết nối bị đóng không chính xác khi bị ngắt kết nối)

Thế nhưng nó vẫn chảy!

Trí nhớ bắt đầu cạn kiệt không phải trong một ngày mà là hai ngày. Hoàn toàn không còn thời gian nữa, vì vậy chúng tôi quyết định triển khai ít chủ đề hơn nhưng trên bốn đại lý. Điều này chắc chắn là đủ trong ít nhất một tuần.

Đã hai ngày trôi qua...

Bây giờ Hazelcast sắp hết bộ nhớ. Nhật ký cho thấy sau một vài ngày thử nghiệm, Hazelcast bắt đầu phàn nàn về việc thiếu bộ nhớ và sau một thời gian, cụm này tan rã và các nút tiếp tục chết từng nút một. Chúng tôi đã kết nối JVisualVM với Hazelcast và thấy một "cái cưa đang lên" - nó thường được gọi là GC, nhưng không thể xóa bộ nhớ.

Cách thức và lý do chúng tôi viết dịch vụ có khả năng mở rộng tải cao cho 1C: Enterprise: Java, PostgreSQL, Hazelcast

Hóa ra trong Hazelcast 3.4, khi xóa bản đồ / multiMap (map.destroy()), bộ nhớ không được giải phóng hoàn toàn:

github.com/hazelcast/hazelcast/issues/6317
github.com/hazelcast/hazelcast/issues/4888

Lỗi này hiện đã được sửa ở phiên bản 3.5, nhưng hồi đó nó đã là một vấn đề. Chúng tôi đã tạo nhiều Bản đồ mới với tên động và xóa chúng theo logic của chúng tôi. Mã trông giống như thế này:

public void join(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.put(auth.getUserId(), auth);
}

public void leave(Authentication auth, String sub) {
    MultiMap<UUID, Authentication> sessions = instance.getMultiMap(sub);
    sessions.remove(auth.getUserId(), auth);

    if (sessions.size() == 0) {
        sessions.destroy();
    }
}

nói:

service.join(auth1, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");
service.join(auth2, "НОВЫЕ_СООБЩЕНИЯ_В_ОБСУЖДЕНИИ_UUID1");

multiMap đã được tạo cho mỗi đăng ký và bị xóa khi không cần thiết. Chúng tôi quyết định sẽ bắt đầu Bản đồ , khóa sẽ là tên của đăng ký và các giá trị sẽ là số nhận dạng phiên (từ đó bạn có thể lấy số nhận dạng người dùng, nếu cần).

public void join(Authentication auth, String sub) {
    addValueToMap(sub, auth.getSessionId());
}

public void leave(Authentication auth, String sub) { 
    removeValueFromMap(sub, auth.getSessionId());
}

Các biểu đồ đã được cải thiện.

Cách thức và lý do chúng tôi viết dịch vụ có khả năng mở rộng tải cao cho 1C: Enterprise: Java, PostgreSQL, Hazelcast

Chúng ta còn học được gì nữa về kiểm thử tải?

  1. JSR223 cần được viết bằng ngôn ngữ Groovy và bao gồm bộ đệm biên dịch - nó nhanh hơn nhiều. Liên kết.
  2. Biểu đồ Jmeter-Plugins dễ hiểu hơn biểu đồ tiêu chuẩn. Liên kết.

Về trải nghiệm của chúng tôi với Hazelcast

Hazelcast là một sản phẩm mới đối với chúng tôi, chúng tôi bắt đầu làm việc với nó từ phiên bản 3.4.1, hiện tại máy chủ sản xuất của chúng tôi đang chạy phiên bản 3.9.2 (tại thời điểm viết bài, phiên bản mới nhất của Hazelcast là 3.10).

tạo ID

Chúng tôi bắt đầu với số nhận dạng số nguyên. Hãy tưởng tượng rằng chúng ta cần một Long khác cho một thực thể mới. Trình tự trong cơ sở dữ liệu không phù hợp, các bảng có liên quan đến phân đoạn - hóa ra có một thông báo ID=1 trong DB1 và ​​một thông báo ID=1 trong DB2, bạn không thể đặt ID này trong Elasticsearch, cũng như trong Hazelcast , nhưng điều tệ nhất là nếu bạn muốn kết hợp dữ liệu từ hai cơ sở dữ liệu thành một (ví dụ: quyết định rằng một cơ sở dữ liệu là đủ cho những người đăng ký này). Bạn có thể thêm một số AtomicLongs vào Hazelcast và giữ bộ đếm ở đó, khi đó hiệu suất lấy ID mới sẽ tăng dầnAndGet cộng với thời gian yêu cầu Hazelcast. Nhưng Hazelcast có thứ tối ưu hơn - FlakeIdGenerator. Khi liên hệ với từng khách hàng, họ sẽ được cung cấp một phạm vi ID, ví dụ: phạm vi ID đầu tiên – từ 1 đến 10, phạm vi thứ hai – từ 000 đến 10, v.v. Giờ đây, khách hàng có thể tự cấp số nhận dạng mới cho đến khi phạm vi được cấp cho nó kết thúc. Nó hoạt động nhanh chóng, nhưng khi bạn khởi động lại ứng dụng (và ứng dụng khách Hazelcast), một trình tự mới sẽ bắt đầu - do đó sẽ bị bỏ qua, v.v. Ngoài ra, các nhà phát triển cũng không thực sự hiểu tại sao ID lại là số nguyên nhưng lại không nhất quán như vậy. Chúng tôi đã cân nhắc mọi thứ và chuyển sang UUID.

Nhân tiện, đối với những ai muốn giống như Twitter, có một thư viện Snowcast như vậy - đây là cách triển khai Snowflake trên Hazelcast. Bạn có thể xem nó ở đây:

github.com/noctarius/snowcast
github.com/twitter/snowflake

Nhưng chúng tôi không còn quan tâm đến nó nữa.

Bản đồ giao dịch.replace

Một điều ngạc nhiên khác: TransactionalMap.replace không hoạt động. Đây là một bài kiểm tra:

@Test
public void replaceInMap_putsAndGetsInsideTransaction() {

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            context.getMap("map").put("key", "oldValue");
            context.getMap("map").replace("key", "oldValue", "newValue");
            
            String value = (String) context.getMap("map").get("key");
            assertEquals("newValue", value);

            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }        
    });
}

Expected : newValue
Actual : oldValue

Tôi đã phải viết bản thay thế của riêng mình bằng getForUpdate:

protected <K,V> boolean replaceInMap(String mapName, K key, V oldValue, V newValue) {
    TransactionalTaskContext context = HazelcastTransactionContextHolder.getContext();
    if (context != null) {
        log.trace("[CACHE] Replacing value in a transactional map");
        TransactionalMap<K, V> map = context.getMap(mapName);
        V value = map.getForUpdate(key);
        if (oldValue.equals(value)) {
            map.put(key, newValue);
            return true;
        }

        return false;
    }
    log.trace("[CACHE] Replacing value in a not transactional map");
    IMap<K, V> map = hazelcastInstance.getMap(mapName);
    return map.replace(key, oldValue, newValue);
}

Kiểm tra không chỉ các cấu trúc dữ liệu thông thường mà còn cả các phiên bản giao dịch của chúng. Điều đó xảy ra là IMap hoạt động nhưng TransactionalMap không còn tồn tại.

Chèn một JAR mới mà không có thời gian chết

Đầu tiên, chúng tôi quyết định ghi lại các đối tượng của lớp mình trong Hazelcast. Ví dụ chúng ta có một lớp Ứng dụng, chúng ta muốn lưu và đọc nó. Cứu:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
map.set(id, application);

Đọc:

IMap<UUID, Application> map = hazelcastInstance.getMap("application");
return map.get(id);

Mọi thứ đang hoạt động. Sau đó, chúng tôi quyết định xây dựng một chỉ mục trong Hazelcast để tìm kiếm theo:

map.addIndex("subscriberId", false);

Và khi viết một thực thể mới, họ bắt đầu nhận được ClassNotFoundException. Hazelcast đã cố gắng thêm vào chỉ mục, nhưng không biết gì về lớp của chúng tôi và muốn cung cấp một JAR với lớp này cho nó. Chúng tôi đã làm đúng như vậy, mọi thứ đều hoạt động, nhưng một vấn đề mới xuất hiện: làm cách nào để cập nhật JAR mà không dừng hoàn toàn cụm? Hazelcast không nhận JAR mới trong quá trình cập nhật từng nút. Tại thời điểm này, chúng tôi quyết định rằng chúng tôi có thể tồn tại mà không cần tìm kiếm chỉ mục. Rốt cuộc, nếu bạn sử dụng Hazelcast làm kho lưu trữ khóa-giá trị thì mọi thứ sẽ hoạt động chứ? Không thực sự. Ở đây một lần nữa hành vi của IMap và TransactionalMap lại khác. Khi IMap không quan tâm, TransactionalMap sẽ báo lỗi.

IMap. Chúng tôi viết 5000 đối tượng, đọc chúng. Mọi thứ đều được mong đợi.

@Test
void get5000() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application");
    UUID subscriberId = UUID.randomUUID();

    for (int i = 0; i < 5000; i++) {
        UUID id = UUID.randomUUID();
        String title = RandomStringUtils.random(5);
        Application application = new Application(id, title, subscriberId);
        
        map.set(id, application);
        Application retrieved = map.get(id);
        assertEquals(id, retrieved.getId());
    }
}

Nhưng nó không hoạt động trong một giao dịch, chúng tôi nhận được ClassNotFoundException:

@Test
void get_transaction() {
    IMap<UUID, Application> map = hazelcastInstance.getMap("application_t");
    UUID subscriberId = UUID.randomUUID();
    UUID id = UUID.randomUUID();

    Application application = new Application(id, "qwer", subscriberId);
    map.set(id, application);
    
    Application retrievedOutside = map.get(id);
    assertEquals(id, retrievedOutside.getId());

    hazelcastInstance.executeTransaction(context -> {
        HazelcastTransactionContextHolder.setContext(context);
        try {
            TransactionalMap<UUID, Application> transactionalMap = context.getMap("application_t");
            Application retrievedInside = transactionalMap.get(id);

            assertEquals(id, retrievedInside.getId());
            return null;
        } finally {
            HazelcastTransactionContextHolder.clearContext();
        }
    });
}

Trong phiên bản 3.8, cơ chế Triển khai Lớp Người dùng đã xuất hiện. Bạn có thể chỉ định một nút chính và cập nhật tệp JAR trên đó.

Bây giờ chúng tôi đã thay đổi hoàn toàn cách tiếp cận của mình: chúng tôi tự tuần tự hóa nó thành JSON và lưu nó trong Hazelcast. Hazelcast không cần biết cấu trúc các lớp của chúng tôi và chúng tôi có thể cập nhật mà không có thời gian chết. Phiên bản của các đối tượng miền được kiểm soát bởi ứng dụng. Các phiên bản khác nhau của ứng dụng có thể chạy cùng lúc và có thể xảy ra tình huống khi ứng dụng mới ghi đối tượng bằng các trường mới nhưng ứng dụng cũ chưa biết về các trường này. Và đồng thời, ứng dụng mới đọc các đối tượng được viết bởi ứng dụng cũ không có trường mới. Chúng tôi xử lý các tình huống như vậy trong ứng dụng, nhưng để đơn giản, chúng tôi không thay đổi hoặc xóa các trường mà chỉ mở rộng các lớp bằng cách thêm các trường mới.

Cách chúng tôi đảm bảo hiệu suất cao

Bốn chuyến đi tới Hazelcast - tốt, hai chuyến đến cơ sở dữ liệu - tệ

Truy cập bộ nhớ đệm để lấy dữ liệu luôn tốt hơn truy cập cơ sở dữ liệu, nhưng bạn cũng không muốn lưu trữ các bản ghi không sử dụng. Chúng tôi để lại quyết định về những gì cần lưu vào bộ nhớ đệm cho đến giai đoạn phát triển cuối cùng. Khi chức năng mới được mã hóa, chúng tôi bật ghi nhật ký tất cả truy vấn trong PostgreSQL (log_min_duration_statement thành 0) và chạy thử nghiệm tải trong 20 phút. Bằng cách sử dụng nhật ký đã thu thập, các tiện ích như pgFouine và pgBadger có thể xây dựng báo cáo phân tích. Trong báo cáo, chúng tôi chủ yếu tìm kiếm các truy vấn chậm và thường xuyên. Đối với các truy vấn chậm, chúng tôi xây dựng kế hoạch thực hiện (GIẢI THÍCH) và đánh giá xem liệu truy vấn đó có thể được tăng tốc hay không. Các yêu cầu thường xuyên cho cùng một dữ liệu đầu vào sẽ phù hợp với bộ đệm. Chúng tôi cố gắng giữ cho các truy vấn “phẳng”, một bảng cho mỗi truy vấn.

Khai thác

SV dưới dạng dịch vụ trực tuyến được đưa vào hoạt động vào mùa xuân năm 2017 và là một sản phẩm riêng biệt, SV được phát hành vào tháng 2017 năm XNUMX (lúc đó ở trạng thái phiên bản beta).

Trong hơn một năm hoạt động, chưa có sự cố nghiêm trọng nào xảy ra trong hoạt động dịch vụ CB trực tuyến. Chúng tôi giám sát dịch vụ trực tuyến thông qua Zabbix, thu thập và triển khai từ Cây tre.

Phân phối máy chủ SV được cung cấp dưới dạng gói gốc: RPM, DEB, MSI. Ngoài ra, đối với Windows, chúng tôi cung cấp một trình cài đặt duy nhất dưới dạng một EXE duy nhất để cài đặt máy chủ, Hazelcast và Elaticsearch trên một máy. Ban đầu chúng tôi gọi phiên bản cài đặt này là phiên bản “demo”, nhưng giờ đây rõ ràng đây là tùy chọn triển khai phổ biến nhất.

Nguồn: www.habr.com

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