您可以从 GAE 数据存储中查询的实体数量是否有限制?
Is there a limit on the number of entities you can query from the GAE datastore?
我的 GCM 端点源自 /github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/GcmEndpoints/root/src/main 处的代码。每个 Android 客户端设备
向端点注册。可以使用此代码向前 10 个注册设备发送消息:
@Api(name = "messaging", version = "v1", namespace = @ApiNamespace(ownerDomain = "${endpointOwnerDomain}", ownerName = "${endpointOwnerDomain}", packagePath="${endpointPackagePath}"))
public class MessagingEndpoint {
private static final Logger log = Logger.getLogger(MessagingEndpoint.class.getName());
/** Api Keys can be obtained from the google cloud console */
private static final String API_KEY = System.getProperty("gcm.api.key");
/**
* Send to the first 10 devices (You can modify this to send to any number of devices or a specific device)
*
* @param message The message to send
*/
public void sendMessage(@Named("message") String message) throws IOException {
if(message == null || message.trim().length() == 0) {
log.warning("Not sending message because it is empty");
return;
}
// crop longer messages
if (message.length() > 1000) {
message = message.substring(0, 1000) + "[...]";
}
Sender sender = new Sender(API_KEY);
Message msg = new Message.Builder().addData("message", message).build();
List<RegistrationRecord> records = ofy().load().type(RegistrationRecord.class).limit(10).list();
for(RegistrationRecord record : records) {
Result result = sender.send(msg, record.getRegId(), 5);
if (result.getMessageId() != null) {
log.info("Message sent to " + record.getRegId());
String canonicalRegId = result.getCanonicalRegistrationId();
if (canonicalRegId != null) {
// if the regId changed, we have to update the datastore
log.info("Registration Id changed for " + record.getRegId() + " updating to " + canonicalRegId);
record.setRegId(canonicalRegId);
ofy().save().entity(record).now();
}
} else {
String error = result.getErrorCodeName();
if (error.equals(Constants.ERROR_NOT_REGISTERED)) {
log.warning("Registration Id " + record.getRegId() + " no longer registered with GCM, removing from datastore");
// if the device is no longer registered with Gcm, remove it from the datastore
ofy().delete().entity(record).now();
}
else {
log.warning("Error when sending message : " + error);
}
}
}
}
}
以上代码发送给前 10 个注册设备。我想发送给所有注册客户。根据查询结果集中的http://objectify-appengine.googlecode.com/svn/branches/allow-parent-filtering/javadoc/com/googlecode/objectify/cmd/Query.html#limit(int) setting limit(0) accomplishes this. But I'm not convinced there will not be a problem for very large numbers of registered clients due to memory constraints or the time it takes to execute the query. https://code.google.com/p/objectify-appengine/source/browse/Queries.wiki?repo=wiki状态"Cursors let you take a "checkpoint”,将checkpoint存储到别处,稍后再从中断处继续。这通常与Task Queue结合使用API 遍历无法在单个请求的 60 秒限制内处理的大型数据集。
注意关于单个请求 60 秒限制的注释。
所以我的问题 - 如果我修改了位于 /github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/GcmEndpoints/root/src/main 的示例代码以通过将 limit(10) 替换为 limit(0) 来请求数据存储中的所有对象,这是否会曾经因为大量对象而失败吗?如果它会失败,大概有多少对象?
如果您尝试检索太多实体,您的查询将超时。您需要在循环中使用游标。
没有人能说出在此超时之前可以检索多少实体 - 这取决于实体的大小、查询的复杂性,最重要的是,循环中还发生了什么。例如,在您的情况下,您可以通过创建任务而不是在循环本身内构建和发送消息来显着加快循环速度(从而在超时之前检索更多实体)。
请注意,默认情况下查询 returns 个实体,每块 20 个 - 如果您有大量实体,则需要增加块大小。
这是一个糟糕的模式,即使有游标。至少,您将达到单个请求的 60 秒硬限制。由于您在 RegistrationRecord
上进行更新,因此您需要一个事务,这会进一步减慢该过程。
这正是任务队列的用途。最好的方法是在两个任务中完成:
- 您的 api 端点立即使 "send message to everyone" 和 returns 入队。
- 第一个任务是 "mapper",它使用纯键查询迭代
RegistrationRecord
。对于每个键,为 "send X message to this record". 排队 "reducer" 任务
- reducer 任务发送消息并(在事务中)执行您的记录更新。
使用 Deferred 实际上代码并不多。
第一个任务立即释放您的客户端,并为您提供 10m 的时间来迭代 RegistrationRecord
键,而不是正常请求的 60 秒限制。如果您拥有分块权和批量队列提交,您应该能够每秒生成数千个 reducer 任务。
这将毫不费力地扩展到数十万用户,并可能使您达到数百万。如果您需要更高的规模,您可以应用 map/reduce 方法来并行化映射。那么这只是一个问题,你想在这个问题上抛出多少实例。
我过去曾使用这种方法一次发送数百万个苹果推送通知,效果很好。任务队列是你的朋友,大量使用它。
我的 GCM 端点源自 /github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/GcmEndpoints/root/src/main 处的代码。每个 Android 客户端设备 向端点注册。可以使用此代码向前 10 个注册设备发送消息:
@Api(name = "messaging", version = "v1", namespace = @ApiNamespace(ownerDomain = "${endpointOwnerDomain}", ownerName = "${endpointOwnerDomain}", packagePath="${endpointPackagePath}"))
public class MessagingEndpoint {
private static final Logger log = Logger.getLogger(MessagingEndpoint.class.getName());
/** Api Keys can be obtained from the google cloud console */
private static final String API_KEY = System.getProperty("gcm.api.key");
/**
* Send to the first 10 devices (You can modify this to send to any number of devices or a specific device)
*
* @param message The message to send
*/
public void sendMessage(@Named("message") String message) throws IOException {
if(message == null || message.trim().length() == 0) {
log.warning("Not sending message because it is empty");
return;
}
// crop longer messages
if (message.length() > 1000) {
message = message.substring(0, 1000) + "[...]";
}
Sender sender = new Sender(API_KEY);
Message msg = new Message.Builder().addData("message", message).build();
List<RegistrationRecord> records = ofy().load().type(RegistrationRecord.class).limit(10).list();
for(RegistrationRecord record : records) {
Result result = sender.send(msg, record.getRegId(), 5);
if (result.getMessageId() != null) {
log.info("Message sent to " + record.getRegId());
String canonicalRegId = result.getCanonicalRegistrationId();
if (canonicalRegId != null) {
// if the regId changed, we have to update the datastore
log.info("Registration Id changed for " + record.getRegId() + " updating to " + canonicalRegId);
record.setRegId(canonicalRegId);
ofy().save().entity(record).now();
}
} else {
String error = result.getErrorCodeName();
if (error.equals(Constants.ERROR_NOT_REGISTERED)) {
log.warning("Registration Id " + record.getRegId() + " no longer registered with GCM, removing from datastore");
// if the device is no longer registered with Gcm, remove it from the datastore
ofy().delete().entity(record).now();
}
else {
log.warning("Error when sending message : " + error);
}
}
}
}
}
以上代码发送给前 10 个注册设备。我想发送给所有注册客户。根据查询结果集中的http://objectify-appengine.googlecode.com/svn/branches/allow-parent-filtering/javadoc/com/googlecode/objectify/cmd/Query.html#limit(int) setting limit(0) accomplishes this. But I'm not convinced there will not be a problem for very large numbers of registered clients due to memory constraints or the time it takes to execute the query. https://code.google.com/p/objectify-appengine/source/browse/Queries.wiki?repo=wiki状态"Cursors let you take a "checkpoint”,将checkpoint存储到别处,稍后再从中断处继续。这通常与Task Queue结合使用API 遍历无法在单个请求的 60 秒限制内处理的大型数据集。
注意关于单个请求 60 秒限制的注释。
所以我的问题 - 如果我修改了位于 /github.com/GoogleCloudPlatform/gradle-appengine-templates/tree/master/GcmEndpoints/root/src/main 的示例代码以通过将 limit(10) 替换为 limit(0) 来请求数据存储中的所有对象,这是否会曾经因为大量对象而失败吗?如果它会失败,大概有多少对象?
如果您尝试检索太多实体,您的查询将超时。您需要在循环中使用游标。
没有人能说出在此超时之前可以检索多少实体 - 这取决于实体的大小、查询的复杂性,最重要的是,循环中还发生了什么。例如,在您的情况下,您可以通过创建任务而不是在循环本身内构建和发送消息来显着加快循环速度(从而在超时之前检索更多实体)。
请注意,默认情况下查询 returns 个实体,每块 20 个 - 如果您有大量实体,则需要增加块大小。
这是一个糟糕的模式,即使有游标。至少,您将达到单个请求的 60 秒硬限制。由于您在 RegistrationRecord
上进行更新,因此您需要一个事务,这会进一步减慢该过程。
这正是任务队列的用途。最好的方法是在两个任务中完成:
- 您的 api 端点立即使 "send message to everyone" 和 returns 入队。
- 第一个任务是 "mapper",它使用纯键查询迭代
RegistrationRecord
。对于每个键,为 "send X message to this record". 排队 "reducer" 任务
- reducer 任务发送消息并(在事务中)执行您的记录更新。
使用 Deferred 实际上代码并不多。
第一个任务立即释放您的客户端,并为您提供 10m 的时间来迭代 RegistrationRecord
键,而不是正常请求的 60 秒限制。如果您拥有分块权和批量队列提交,您应该能够每秒生成数千个 reducer 任务。
这将毫不费力地扩展到数十万用户,并可能使您达到数百万。如果您需要更高的规模,您可以应用 map/reduce 方法来并行化映射。那么这只是一个问题,你想在这个问题上抛出多少实例。
我过去曾使用这种方法一次发送数百万个苹果推送通知,效果很好。任务队列是你的朋友,大量使用它。