Sử dụng kho lưu trữ git làm chương trình phụ trợ cơ sở dữ liệu


119

Tôi đang thực hiện một dự án liên quan đến cơ sở dữ liệu tài liệu có cấu trúc. Tôi có một cây danh mục (~ 1000 danh mục, lên đến ~ 50 danh mục trên mỗi cấp độ), mỗi danh mục chứa hàng nghìn (lên đến ~ 10000) tài liệu có cấu trúc. Mỗi tài liệu là vài kilobyte dữ liệu ở một số dạng có cấu trúc (Tôi thích YAML hơn, nhưng nó cũng có thể là JSON hoặc XML).

Người dùng hệ thống này thực hiện một số loại hoạt động:

  • lấy các tài liệu này bằng ID
  • tìm kiếm tài liệu bằng một số thuộc tính có cấu trúc bên trong chúng
  • chỉnh sửa tài liệu (tức là thêm / bớt / đổi tên / hợp nhất); mỗi thao tác chỉnh sửa phải được ghi lại thành một giao dịch với một số nhận xét
  • xem lịch sử các thay đổi đã ghi cho tài liệu cụ thể (bao gồm xem ai, khi nào và tại sao đã thay đổi tài liệu, tải phiên bản trước đó - và có thể hoàn nguyên về phiên bản này nếu được yêu cầu)

Tất nhiên, giải pháp truyền thống sẽ là sử dụng một số loại cơ sở dữ liệu tài liệu (chẳng hạn như CouchDB hoặc Mongo) cho vấn đề này - tuy nhiên, điều kiểm soát phiên bản (lịch sử) này đã khiến tôi nảy sinh ý tưởng hoang đường - tại sao tôi không nên sử dụng gitkho lưu trữ làm cơ sở dữ liệu phụ trợ cho ứng dụng này?

Ngay từ cái nhìn đầu tiên, nó có thể được giải quyết như thế này:

  • thể loại = thư mục, tài liệu = tệp
  • nhận tài liệu theo ID => thay đổi thư mục + đọc tệp trong bản sao đang hoạt động
  • chỉnh sửa tài liệu với chỉnh sửa nhận xét => thực hiện cam kết của nhiều người dùng + lưu trữ thông báo cam kết
  • history => nhật ký git bình thường và truy xuất các giao dịch cũ hơn
  • tìm kiếm => đó là một phần phức tạp hơn một chút, tôi đoán nó sẽ yêu cầu xuất định kỳ một danh mục vào cơ sở dữ liệu quan hệ với việc lập chỉ mục các cột mà chúng tôi sẽ cho phép tìm kiếm theo

Có bất kỳ cạm bẫy phổ biến nào khác trong giải pháp này không? Có ai đã cố gắng triển khai phần phụ trợ như vậy chưa (tức là cho bất kỳ khuôn khổ phổ biến nào - RoR, node.js, Django, CakePHP)? Giải pháp này có bất kỳ tác động nào có thể xảy ra đối với hiệu suất hoặc độ tin cậy - tức là nó đã được chứng minh rằng git sẽ chậm hơn nhiều so với các giải pháp cơ sở dữ liệu truyền thống hay sẽ có bất kỳ cạm bẫy nào về khả năng mở rộng / độ tin cậy không? Tôi cho rằng một cụm các máy chủ đẩy / kéo kho lưu trữ của nhau phải khá mạnh mẽ và đáng tin cậy.

Về cơ bản, hãy cho tôi biết nếu giải pháp này sẽ hoạt động và tại sao nó sẽ làm được hoặc không?


Câu trả lời:


58

Trả lời câu hỏi của riêng tôi không phải là điều tốt nhất nên làm, nhưng cuối cùng tôi đã bỏ ý định, tôi muốn chia sẻ về cơ sở lý luận đã phù hợp với trường hợp của tôi. Tôi muốn nhấn mạnh rằng cơ sở lý luận này có thể không áp dụng cho tất cả các trường hợp, vì vậy kiến ​​trúc sư quyết định.

Nói chung, điểm chính đầu tiên mà câu hỏi của tôi bỏ sót là tôi đang xử lý hệ thống nhiều người dùng hoạt động song song, đồng thời, sử dụng máy chủ của tôi với một máy khách mỏng (tức là chỉ một trình duyệt web). Bằng cách này, tôi phải duy trì trạng thái cho tất cả chúng. Có một số cách tiếp cận cho cách này, nhưng tất cả chúng đều quá khó về tài nguyên hoặc quá phức tạp để thực hiện (và do đó, loại bỏ mục đích ban đầu là giảm tải tất cả những thứ khó triển khai xuống git ngay từ đầu):

  • Phương pháp tiếp cận "cùn": 1 người dùng = 1 trạng thái = 1 bản sao hoạt động đầy đủ của kho lưu trữ mà máy chủ duy trì cho người dùng. Ngay cả khi chúng ta đang nói về cơ sở dữ liệu tài liệu khá nhỏ (ví dụ: 100 MiB) với ~ 100K người dùng, việc duy trì bản sao kho lưu trữ đầy đủ cho tất cả chúng sẽ khiến việc sử dụng đĩa diễn ra nhanh chóng (tức là 100K người dùng nhân với 100MiB ~ 10 TiB) . Điều tồi tệ hơn nữa, việc sao chép kho lưu trữ 100 MiB mỗi lần mất vài giây, ngay cả khi được thực hiện trong thao tác khá hiệu quả (tức là không sử dụng bằng git và giải nén nội dung đóng gói), điều này không thể chấp nhận được, IMO. Và thậm chí tệ hơn - mọi chỉnh sửa mà chúng tôi áp dụng cho cây chính phải được kéo đến kho lưu trữ của mọi người dùng, đó là (1) tài nguyên hog, (2) có thể dẫn đến xung đột chỉnh sửa chưa được giải quyết trong trường hợp chung.

    Về cơ bản, nó có thể tệ bằng O (số lần chỉnh sửa × dữ liệu × số người dùng) về mức sử dụng đĩa và việc sử dụng đĩa như vậy tự động có nghĩa là mức sử dụng CPU khá cao.

  • Phương pháp tiếp cận "Chỉ người dùng đang hoạt động": chỉ duy trì bản sao hoạt động cho người dùng đang hoạt động. Bằng cách này, bạn thường không lưu trữ toàn bộ repo-clone-per-user, mà là:

    • Khi người dùng đăng nhập, bạn sao chép kho lưu trữ. Mất vài giây và ~ 100 MiB dung lượng đĩa cho mỗi người dùng đang hoạt động.
    • Khi người dùng tiếp tục làm việc trên trang web, anh ta sẽ làm việc với bản sao làm việc đã cho.
    • Khi người dùng đăng xuất, bản sao kho lưu trữ của anh ta được sao chép trở lại kho lưu trữ chính dưới dạng một nhánh, do đó chỉ lưu trữ "các thay đổi chưa được áp dụng" của anh ta, nếu có, khá tiết kiệm không gian.

    Do đó, mức sử dụng đĩa trong trường hợp này đạt đỉnh O (số lần chỉnh sửa × dữ liệu × số người dùng đang hoạt động), thường ít hơn ~ 100..1000 lần so với tổng số người dùng, nhưng nó làm cho việc đăng nhập / đăng xuất phức tạp hơn và chậm hơn , vì nó liên quan đến việc sao chép nhánh của mỗi người dùng trên mỗi lần đăng nhập và kéo những thay đổi này trở lại khi đăng xuất hoặc hết hạn phiên (điều này nên được thực hiện theo giao dịch => thêm một lớp phức tạp khác). Trong trường hợp của tôi, nó giảm 10 TiB sử dụng đĩa xuống 10..100 GiB, điều đó có thể chấp nhận được, nhưng, một lần nữa, chúng ta đang nói về cơ sở dữ liệu khá nhỏ gồm 100 MiB.

  • Phương pháp "thanh toán thưa thớt": tạo "thanh toán thưa thớt" thay vì sao chép toàn bộ repo cho mỗi người dùng đang hoạt động không giúp ích nhiều. Nó có thể tiết kiệm ~ 10 lần dung lượng đĩa sử dụng, nhưng với chi phí tải CPU / đĩa cao hơn nhiều trên các hoạt động liên quan đến lịch sử, loại này sẽ giết chết mục đích.

  • Phương pháp tiếp cận "nhóm công nhân": thay vì thực hiện các bản sao toàn diện cho người đang hoạt động, chúng tôi có thể giữ một nhóm các bản sao "công nhân", sẵn sàng sử dụng. Bằng cách này, mỗi khi người dùng đăng nhập, anh ta chiếm một "công nhân", kéo chi nhánh của anh ta từ repo chính ở đó và khi anh ta đăng xuất, anh ta giải phóng "công nhân", thao tác này thực hiện khôi phục cài đặt gốc git để trở lại bình thường bản sao repo chính, sẵn sàng được sử dụng bởi một người dùng khác đang đăng nhập. Không giúp ích nhiều cho việc sử dụng đĩa (nó vẫn khá cao - chỉ bản sao đầy đủ cho mỗi người dùng đang hoạt động), nhưng ít nhất nó giúp đăng nhập / đăng xuất nhanh hơn, vì thậm chí còn phức tạp hơn.

Điều đó nói rằng, lưu ý rằng tôi đã cố ý tính toán số lượng cơ sở dữ liệu và cơ sở người dùng khá nhỏ: 100 nghìn người dùng, 1 nghìn người dùng đang hoạt động, tổng cơ sở dữ liệu 100 MiB + lịch sử các chỉnh sửa, 10 MiB bản sao làm việc. Nếu bạn xem xét các dự án tìm nguồn cung ứng cộng đồng nổi bật hơn, thì có những con số cao hơn ở đó:

│              │ Users │ Active users │ DB+edits │ DB only │
├──────────────┼───────┼──────────────┼──────────┼─────────┤
│ MusicBrainz  │  1.2M │     1K/week  │   30 GiB │  20 GiB │
│ en.wikipedia │ 21.5M │   133K/month │    3 TiB │  44 GiB │
│ OSM          │  1.7M │    21K/month │  726 GiB │ 480 GiB │

Rõ ràng, đối với lượng dữ liệu / hoạt động đó, cách tiếp cận này sẽ hoàn toàn không thể chấp nhận được.

Nói chung, nó sẽ hoạt động, nếu người ta có thể sử dụng trình duyệt web như một ứng dụng khách "dày", tức là phát hành các hoạt động git và lưu trữ khá nhiều thanh toán đầy đủ ở phía máy khách chứ không phải phía máy chủ.

Ngoài ra còn có những điểm khác mà tôi đã bỏ qua, nhưng chúng không tệ lắm so với điểm đầu tiên:

  • Mô hình có trạng thái chỉnh sửa của người dùng "dày" gây tranh cãi về các ORM thông thường, chẳng hạn như ActiveRecord, Hibernate, DataMapper, Tower, v.v.
  • Nhiều như tôi đã tìm kiếm, không có cơ sở mã miễn phí nào hiện có để thực hiện cách tiếp cận đó với git từ các khung công tác phổ biến.
  • Có ít nhất một dịch vụ bằng cách nào đó quản lý để làm điều đó một cách hiệu quả - đó rõ ràng là github - nhưng, than ôi, cơ sở mã của họ là nguồn đóng và tôi thực sự nghi ngờ rằng họ không sử dụng máy chủ git bình thường / kỹ thuật lưu trữ repo bên trong, tức là về cơ bản chúng đã được triển khai git "dữ liệu lớn" thay thế.

Vì vậy, điểm mấu chốt : điều đó có thể, nhưng đối với hầu hết các cách sử dụng hiện tại, nó sẽ không ở đâu gần giải pháp tối ưu. Cuộn triển khai tài liệu-chỉnh sửa-lịch sử-thành SQL của riêng bạn hoặc cố gắng sử dụng bất kỳ cơ sở dữ liệu tài liệu hiện có nào có lẽ sẽ là giải pháp thay thế tốt hơn.


16
Có lẽ đến bữa tiệc hơi muộn, nhưng tôi đã có một yêu cầu tương tự với điều này và thực sự đã đi xuống git-route. Sau một số tìm hiểu với nội bộ git, tôi đã tìm ra cách để làm cho nó hoạt động. Ý tưởng là làm việc với một kho lưu trữ trống. Có một số hạn chế, nhưng tôi thấy nó có thể làm được. Tôi đã viết tất cả mọi thứ trong một bài đăng, bạn có thể muốn kiểm tra (nếu bất cứ điều gì, vì lợi ích của): kenneth-truyers.net/2016/10/13/git-nosql-database
Kenneth

Một lý do khác để tôi không làm điều này là khả năng truy vấn. Các kho lưu trữ tài liệu thường lập chỉ mục tài liệu, giúp bạn dễ dàng tìm kiếm trong đó. Điều này sẽ không được chuyển tiếp thẳng với git.
FrankyHollywood

12

Một cách tiếp cận thú vị thực sự. Tôi muốn nói rằng nếu bạn cần lưu trữ dữ liệu, hãy sử dụng một cơ sở dữ liệu, không phải một kho lưu trữ mã nguồn, được thiết kế cho một nhiệm vụ rất cụ thể. Nếu bạn có thể sử dụng Git out-of-the-box thì không sao, nhưng có thể bạn cần phải xây dựng một lớp kho lưu trữ tài liệu trên đó. Vì vậy, bạn cũng có thể xây dựng nó trên cơ sở dữ liệu truyền thống, phải không? Và nếu đó là quyền kiểm soát phiên bản tích hợp mà bạn quan tâm, tại sao không chỉ sử dụng một trong những công cụ lưu trữ tài liệu nguồn mở ? Có rất nhiều để lựa chọn.

Chà, nếu bạn quyết định sử dụng chương trình phụ trợ Git, thì về cơ bản, nó sẽ hoạt động cho các yêu cầu của bạn nếu bạn triển khai nó như mô tả. Nhưng:

1) Bạn đã đề cập đến "cụm máy chủ đẩy / kéo lẫn nhau" - Tôi đã nghĩ về nó một lúc và vẫn không chắc chắn. Bạn không thể đẩy / kéo một số repo như một hoạt động nguyên tử. Tôi tự hỏi liệu có khả năng xảy ra một số lộn xộn hợp nhất trong quá trình làm việc đồng thời hay không.

2) Có thể bạn không cần nó, nhưng một chức năng rõ ràng của kho tài liệu mà bạn không liệt kê là kiểm soát truy cập. Bạn có thể hạn chế quyền truy cập vào một số đường dẫn (= danh mục) thông qua mô-đun con, nhưng có thể bạn sẽ không thể dễ dàng cấp quyền truy cập ở cấp tài liệu.


11

giá trị 2 xu của tôi. Một chút khao khát nhưng ...... Tôi đã có một yêu cầu tương tự trong một trong những dự án ươm tạo của mình. Tương tự như của bạn, các yêu cầu chính của tôi trong đó có cơ sở dữ liệu tài liệu (xml trong trường hợp của tôi), với lập phiên bản tài liệu. Nó dành cho một hệ thống nhiều người dùng với nhiều trường hợp sử dụng cộng tác. Sở thích của tôi là sử dụng các giải pháp mã nguồn mở có sẵn hỗ trợ hầu hết các yêu cầu chính.

Để cắt theo đuổi, tôi không thể tìm thấy bất kỳ sản phẩm nào cung cấp cả hai, theo cách đủ khả năng mở rộng (số lượng người dùng, khối lượng sử dụng, tài nguyên lưu trữ và máy tính). Tôi thiên về git cho tất cả các khả năng đầy hứa hẹn và (có thể xảy ra) giải pháp mà người ta có thể tạo ra từ nó. Khi tôi đùa giỡn với tùy chọn git nhiều hơn, việc chuyển từ góc độ người dùng đơn lẻ sang góc độ người dùng đa (milli) trở thành một thách thức rõ ràng. Thật không may, tôi đã không thực hiện phân tích hiệu suất đáng kể như bạn đã làm. (.. lười biếng / bỏ sớm .... cho phiên bản 2, câu thần chú) Power to you !. Dù sao đi nữa, ý tưởng thành kiến ​​của tôi đã chuyển sang phương án thay thế tiếp theo (vẫn thiên vị): một tập hợp các công cụ tốt nhất trong các lĩnh vực, cơ sở dữ liệu và kiểm soát phiên bản riêng biệt của chúng.

Trong khi vẫn đang trong quá trình làm việc (... và hơi bị bỏ quên), phiên bản biến hình chỉ đơn giản là thế này.

  • trên giao diện người dùng: (giao diện người dùng) sử dụng cơ sở dữ liệu để lưu trữ cấp 1 (giao tiếp với các ứng dụng người dùng)
  • trên phần phụ trợ, sử dụng hệ thống kiểm soát phiên bản (VCS) (như git) để thực hiện lập phiên bản của các đối tượng dữ liệu trong cơ sở dữ liệu

Về bản chất, việc thêm một plugin kiểm soát phiên bản vào cơ sở dữ liệu, với một số keo tích hợp, bạn có thể phải phát triển, nhưng có thể dễ dàng hơn nhiều.

Cách nó sẽ (được cho là) ​​hoạt động là việc trao đổi dữ liệu giao diện đa người dùng chính thông qua cơ sở dữ liệu. DBMS sẽ xử lý tất cả các vấn đề phức tạp và thú vị như đa người dùng, đồng thời e, hoạt động nguyên tử, v.v. Trên phần phụ trợ, VCS sẽ thực hiện kiểm soát phiên bản trên một tập hợp các đối tượng dữ liệu (không có vấn đề đồng thời hoặc nhiều người dùng). Đối với mỗi giao dịch hiệu quả trên cơ sở dữ liệu, việc kiểm soát phiên bản chỉ được thực hiện trên các bản ghi dữ liệu đã thay đổi hiệu quả.

Đối với keo giao diện, nó sẽ ở dạng một chức năng liên kết đơn giản giữa cơ sở dữ liệu và VCS. Về mặt thiết kế, cách tiếp cận đơn giản sẽ là giao diện hướng sự kiện, với cập nhật dữ liệu từ cơ sở dữ liệu sẽ kích hoạt các thủ tục kiểm soát phiên bản (gợi ý: giả sử Mysql, sử dụng trình kích hoạt và sys_exec () blah blah ...). Về mức độ phức tạp của việc triển khai, nó sẽ bao gồm từ đơn giản và hiệu quả (ví dụ: script) đến phức tạp và tuyệt vời (một số giao diện kết nối được lập trình). Tất cả phụ thuộc vào mức độ điên rồ bạn muốn làm với nó và số vốn mồ hôi bạn sẵn sàng bỏ ra. Tôi nghĩ rằng kịch bản đơn giản sẽ làm nên điều kỳ diệu. Và để truy cập vào kết quả cuối cùng, các phiên bản dữ liệu khác nhau, một giải pháp thay thế đơn giản là điền một bản sao của cơ sở dữ liệu (bản sao của cấu trúc cơ sở dữ liệu) với dữ liệu được tham chiếu bởi thẻ phiên bản / id / hash trong VCS. một lần nữa, bit này sẽ là một công việc truy vấn / dịch / bản đồ đơn giản của một giao diện.

Vẫn còn một số thách thức và ẩn số cần được xử lý, nhưng tôi cho rằng tác động và mức độ liên quan của hầu hết những thách thức này sẽ phụ thuộc phần lớn vào các yêu cầu ứng dụng và trường hợp sử dụng của bạn. Một số có thể không phải là vấn đề. Một số vấn đề bao gồm kết hợp hiệu suất giữa 2 mô-đun chính, cơ sở dữ liệu và VCS, cho một ứng dụng có hoạt động cập nhật dữ liệu tần suất cao, Chia tỷ lệ tài nguyên (khả năng lưu trữ và xử lý) theo thời gian ở phía git dưới dạng dữ liệu và người dùng phát triển: ổn định, theo cấp số nhân hoặc cuối cùng là bình nguyên

Trong số các loại cocktail ở trên, đây là thứ tôi hiện đang pha

  • sử dụng Git cho VCS (ban đầu được coi là CVS cũ tốt do chỉ sử dụng các tập thay đổi hoặc delta giữa 2 phiên bản)
  • bằng cách sử dụng mysql (do bản chất dữ liệu của tôi có cấu trúc cao, xml với các lược đồ xml nghiêm ngặt)
  • đùa giỡn với MongoDB (để thử cơ sở dữ liệu NoSQl, cơ sở dữ liệu này khớp chặt chẽ với cấu trúc cơ sở dữ liệu gốc được sử dụng trong git)

Một số thông tin thú vị - git thực sự thực hiện những việc rõ ràng để tối ưu hóa lưu trữ, chẳng hạn như nén và chỉ lưu trữ các delta giữa các bản sửa đổi của các đối tượng - CÓ, git chỉ lưu trữ các tập thay đổi hoặc delta giữa các bản sửa đổi của các đối tượng dữ liệu, nó có thể áp dụng được ở đâu (nó biết khi nào và như thế nào). Tham khảo: packfiles, sâu trong ruột của Git internals - Đánh giá về lưu trữ đối tượng của git (hệ thống tệp có thể địa chỉ theo nội dung), cho thấy những điểm tương đồng (từ quan điểm khái niệm) với cơ sở dữ liệu noSQL như mongoDB. Một lần nữa, với chi phí đổ mồ hôi, nó có thể cung cấp nhiều khả năng thú vị hơn để tích hợp 2 và điều chỉnh hiệu suất

Nếu bạn hiểu được điều này, hãy để tôi xem điều trên có thể áp dụng cho trường hợp của bạn không và giả sử như vậy, nó sẽ giải quyết vấn đề như thế nào đối với một số khía cạnh trong phân tích hiệu suất toàn diện cuối cùng của bạn


4

Tôi đã triển khai một thư viện Ruby ở trên cùng libgit2, làm cho điều này khá dễ dàng để thực hiện và khám phá. Có một số hạn chế rõ ràng, nhưng nó cũng là một hệ thống khá giải phóng vì bạn nhận được chuỗi công cụ git đầy đủ.

Tài liệu bao gồm một số ý tưởng về hiệu suất, sự cân bằng, v.v.


2

Như bạn đã đề cập, trường hợp nhiều người dùng phức tạp hơn một chút để xử lý. Một giải pháp khả thi là sử dụng các tệp chỉ mục Git dành riêng cho người dùng dẫn đến

  • không cần các bản sao làm việc riêng biệt (việc sử dụng đĩa bị hạn chế đối với các tệp đã thay đổi)
  • không cần công việc chuẩn bị tốn thời gian (mỗi phiên người dùng)

Bí quyết là kết hợp GIT_INDEX_FILEbiến môi trường của Git với các công cụ để tạo các cam kết Git theo cách thủ công:

Một phác thảo giải pháp sau (các băm SHA1 thực tế bị bỏ qua khỏi các lệnh):

# Initialize the index
# N.B. Use the commit hash since refs might changed during the session.
$ GIT_INDEX_FILE=user_index_file git reset --hard <starting_commit_hash>

#
# Change data and save it to `changed_file`
#

# Save changed data to the Git object database. Returns a SHA1 hash to the blob.
$ cat changed_file | git hash-object -t blob -w --stdin
da39a3ee5e6b4b0d3255bfef95601890afd80709

# Add the changed file (using the object hash) to the user-specific index
# N.B. When adding new files, --add is required
$ GIT_INDEX_FILE=user_index_file git update-index --cacheinfo 100644 <changed_data_hash> path/to/the/changed_file

# Write the index to the object db. Returns a SHA1 hash to the tree object
$ GIT_INDEX_FILE=user_index_file git write-tree
8ea32f8432d9d4fa9f9b2b602ec7ee6c90aa2d53

# Create a commit from the tree. Returns a SHA1 hash to the commit object
# N.B. Parent commit should the same commit as in the first phase.
$ echo "User X updated their data" | git commit-tree <new_tree_hash> -p <starting_commit_hash>
3f8c225835e64314f5da40e6a568ff894886b952

# Create a ref to the new commit
git update-ref refs/heads/users/user_x_change_y <new_commit_hash>

Tùy thuộc vào dữ liệu của bạn, bạn có thể sử dụng cron job để hợp nhất các ref mới vào masternhưng giải quyết xung đột được cho là phần khó nhất ở đây.

Những ý tưởng để làm cho nó dễ dàng hơn được hoan nghênh.


Đó thường là một cách tiếp cận chẳng dẫn đến đâu, trừ khi bạn muốn có một khái niệm đầy đủ về giao dịch và giao diện người dùng để giải quyết xung đột thủ công. Ý tưởng chung cho các xung đột là làm cho người dùng giải quyết nó ngay khi cam kết (nghĩa là "xin lỗi, người khác đã chỉnh sửa tài liệu mà bạn đang chỉnh sửa -> vui lòng xem các chỉnh sửa của anh ấy và các chỉnh sửa của bạn rồi hợp nhất chúng"). Khi bạn cho phép hai người dùng cam kết thành công và sau đó phát hiện ra trong cronjob không đồng bộ rằng mọi thứ đã đi xuống phía nam, nói chung là không có ai để giải quyết mọi thứ.
GreyCat vào
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.