12/08/2018, 16:31

Cải thiện hiệu suất khi dùng GraphQL trong Rails

Với việc sử dụng GraphQL, tốc độ truy xuất vào APi của ứng dụng đã nhanh hơn cách thông thường, tuy nhiên chúng ta vẫn cần phải cải thiên cho chúng. Trong phần này, chúng ta sẽ xem xét ba cách để tránh các vấn đề về hiệu suất với GraphQL trong ứng dụng Rails và sau đó là dùng một công cụ để giúp ...

Với việc sử dụng GraphQL, tốc độ truy xuất vào APi của ứng dụng đã nhanh hơn cách thông thường, tuy nhiên chúng ta vẫn cần phải cải thiên cho chúng.
Trong phần này, chúng ta sẽ xem xét ba cách để tránh các vấn đề về hiệu suất với GraphQL trong ứng dụng Rails và sau đó là dùng một công cụ để giúp theo dõi những truy vấn nào đang được thực thi với GraphQL API của chúng ta.

  • Tránh truy vẫn N + 1 Giả sử chúng ta cần truy vấn thông tin về tiền thuê cùng với chủ sở hữu của phòng thuê, chúng ta sẽ phải mất tổng cộng 6 truy vấn, 1 truy vẫn lấy thông tin về phòng thuê và 5 truy vấn khác để tìm chủ sở hữu cho từng phòng thuê
query {
  rentals {
    id
    owner {
      name
    }
  }
}

Ở đây chúng ta đang lấy tiền thuê cộng với tên chủ sở hữu. Nếu đây là kết thúc REST, chúng ta sẽ biết trước API sẽ trả lại tiền thuê cộng với chủ sở hữu, vì vậy chúng ta có thể thêm một số yêu cầu nạp vào truy vấn .

Tuy nhiên cái này sẽ không làm việc ở đây bởi vì chúng ta không biết những gì client sẽ yêu cầu. Chúng ta không muốn luôn luôn phải tải chủ sở hữu, nếu client không yêu cầu chúng? mà GraphQL được dùng để giải quyết vấn đề tìm kiếm và tải xuống. Console log sẽ như sau:

Rental Load (0.9ms)  SELECT "rentals".* FROM "rentals"
User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 120], ["LIMIT", 1]]
User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 116], ["LIMIT", 1]]
User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 116], ["LIMIT", 1]]
User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 117], ["LIMIT", 1]]
User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 114], ["LIMIT", 1]]

Để giải quyết vấn đề này chúng ta chỉ cần sử dụng 1 gem là graphql-batch. Về cơ bản, gem cho phép chúng ta lấy hàng loạt ID người dùng và thực hiện một truy vấn để tìm tất cả các ID người dùng cùng một lúc. Những gì chúng ta cần làm là sửa đổi quá trình resove như sau:

# app/graphql/types/rental_type.rb
field :owner, Types::UserType do
  resolve -> (obj, args, context) { RecordLoader.for(User).load(obj.user_id) }
end

Ngoài việc thêm dòng này vào trong GraphQL :: Batch, chúng ta cũng cần phải tạo class RecordLoader:

# app/graphql/record_loader.rb
class RecordLoader < GraphQL::Batch::Loader
  def initialize(model)
    @model = model
  end

  def perform(ids)
    @model.where(id: ids).each { |record| fulfill(record.id, record) }
    ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
  end
end

và console log chúng ta bây giờ sẽ như thế này

Rental Load (0.5ms)  SELECT  "rentals".* FROM "rentals" ORDER BY "rentals"."id" DESC LIMIT $1  [["LIMIT", 20]]
User Load (0.4ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (124, 125, 115, 120, 122, 121, 117, 118, 112, 113, 111, 119, 123)

Điều này làm việc rất tốt cho các mối quan hệ belong_to, nhưng chúng ta sẽ gặp khó khăn với has_many. Ví dụ: nếu chúng ta muốn đặt chỗ cho mỗi thuê, chúng sẽ tạo ra các truy vấn N + 1.và để giải quyết vấn đề này chúng ta cần thêm enable_preloading vào schema.

field :bookings, !types[Types::BookingType] do
  preload :bookings
  resolve -> (obj, args, ctx) { obj.bookings }
end

Truy vấn bằng GraphQL sẽ như sau

query {
  rentals {
    id
    owner {
      name
    }
    bookings {
      guest {
        name
      }
    }
  }
}
  • Tránh truy vấn quá phức tạp Như ví dụ trên, khi chúng ta truy vấn lồng nhau nhiều hơn và sâu hơn thì chúng ta sẽ ảnh hưởng đến hiệu suất máy chủ, như truy vấn sau
query {
  rentals {
    id
    bookings {
      id
      guest {
        id
        bookings {
          id
        }
      }
    }
  }
}

Truy vấn trên mỗi khi tăng thếm một lần truy vấn sâu hơn, thì tốc độ truy vấn càng ngày càng chậm từ mấy lần đến hơn chục lần, mặc dù là đã tránh truy vấn N + 1. Do vậy chúng ta nên setting số lần lồng nhau của một truy vấn để tránh server quá tải, và mở rộng nếu chúng ta cần.

Việc thiết lập này được thực hiện băng cách gắn giá tri cho max_depth trong class schema của graphql

# app/graphql/landbnb_schema.rb
LandbnbSchema = GraphQL::Schema.define do
  max_depth 4 
  use GraphQL::Batch
  enable_preloading

  mutation(Types::MutationType)
  query(Types::QueryType)
end
  • Chấp nhận thời gian chờ: Với việc truy vấn có thể lồng nhau, để đảm bảo server có thể thực hiện truy vấn trong thời gian quy định, chúng ta nên thiết lập giá trị thời gian chờ cho truy vấn
#app/graphql/landbnb_schema.rb
LandbnbSchema.middleware <<
  GraphQL::Schema::TimeoutMiddleware.new(max_seconds: 2) do |err, query|
    Rails.logger.info("GraphQL Timeout: #{query.query_string}")
  end
0