Recurso de assinatura
Ao consumir dados de um tópico do DataHub, você precisa gerenciar seus próprios offsets de consumo para retomar o processamento após uma falha na aplicação. Isso exige salvar o progresso e garantir alta disponibilidade no service de armazenamento de offsets, o que aumenta a complexidade da sua aplicação. Para simplificar esse processo, o DataHub oferece um service de assinatura que armazena os offsets de consumo no lado do servidor. Com poucas etapas de configuração e código mínimo, você obtém um service de gerenciamento de offsets com alta disponibilidade, operando de forma transparente para sua aplicação. Esse service também oferece recursos flexíveis de redefinição de offset, compatíveis com semântica de consumo at-least-once. Por exemplo, se você identificar um erro de processamento que afetou dados de um período específico e precisar reprocessá-los, basta redefinir o offset para o horário correspondente. Sua aplicação detecta essa alteração automaticamente e reprocessa os dados sem necessidade de reinicialização.
Criar uma assinatura
Verifique se sua conta tem permissão para criar uma assinatura em um tópico do projeto especificado. Para mais detalhes, consulte a documentação de Controle de Permissões. Siga estas etapas:
-
Abra a página Topic, clique em + Subscription no canto superior direito, preencha os detalhes da assinatura e clique em Create.
Subscription Application: nome da aplicação que utiliza esta assinatura.
Description: descrição detalhada da assinatura.
Clique em no botão de pesquisa em Consumption Checkpoint para visualizar o status de consumo de todos os shards.
Exemplo de uso
O recurso de assinatura armazena offsets. Embora seja independente das funções de leitura e escrita do DataHub (consulte a documentação do Java SDK), ele é frequentemente utilizado em conjunto com elas quando há necessidade de armazenar offsets de consumo após a leitura de dados.
// Example of consuming data and committing offsets during the process.
public void offset_consumption(int maxRetry) {
String endpoint = "<YourEndPoint>";
String accessId = "<YourAccessId>";
String accessKey = "<YourAccessKey>";
String projectName = "<YourProjectName>";
String topicName = "<YourTopicName>";
String subId = "<YourSubId>";
String shardId = "0";
List<String> shardIds = Arrays.asList(shardId);
// Create a DatahubClient instance.
DatahubClient datahubClient = DatahubClientBuilder.newBuilder()
.setDatahubConfig(
new DatahubConfig(endpoint,
// Whether to enable binary transfer. This feature is supported by the server since version 2.12.
new AliyunAccount(accessId, accessKey), true))
.build();
RecordSchema schema = datahubClient.getTopic(projectName, topicName).getRecordSchema();
OpenSubscriptionSessionResult openSubscriptionSessionResult = datahubClient.openSubscriptionSession(projectName, topicName, subId, shardIds);
SubscriptionOffset subscriptionOffset = openSubscriptionSessionResult.getOffsets().get(shardId);
// 1. Get the cursor for the current offset. If the current offset has expired or has never been consumed, get the cursor for the first record within the lifecycle.
String cursor = "";
// A sequence number less than 0 indicates that the shard has not been consumed.
if (subscriptionOffset.getSequence() < 0) {
// Get the cursor for the first record within the lifecycle.
cursor = datahubClient.getCursor(projectName, topicName, shardId, CursorType.OLDEST).getCursor();
} else {
// Get the cursor for the next record.
long nextSequence = subscriptionOffset.getSequence() + 1;
try {
// Getting a cursor using SEQUENCE may throw a SeekOutOfRangeException, which indicates that the data at the current cursor has expired.
cursor = datahubClient.getCursor(projectName, topicName, shardId, CursorType.SEQUENCE, nextSequence).getCursor();
} catch (SeekOutOfRangeException e) {
// Get the cursor for the first record within the lifecycle.
cursor = datahubClient.getCursor(projectName, topicName, shardId, CursorType.OLDEST).getCursor();
}
}
// 2. Read records and save the offset. This example demonstrates reading tuple data and committing the offset every 1,000 records.
long recordCount = 0L;
// Read 1,000 records at a time.
int fetchNum = 1000;
int retryNum = 0;
int commitNum = 1000;
while (retryNum < maxRetry) {
try {
GetRecordsResult getRecordsResult = datahubClient.getRecords(projectName, topicName, shardId, schema, cursor, fetchNum);
if (getRecordsResult.getRecordCount() <= 0) {
// No data. Sleep and try again.
System.out.println("no data, sleep 1 second");
Thread.sleep(1000);
continue;
}
for (RecordEntry recordEntry : getRecordsResult.getRecords()) {
// Process the data.
TupleRecordData data = (TupleRecordData) recordEntry.getRecordData();
System.out.println("field1:" + data.getField("field1") + "\t"
+ "field2:" + data.getField("field2"));
// After processing the data, update the offset.
recordCount++;
subscriptionOffset.setSequence(recordEntry.getSequence());
subscriptionOffset.setTimestamp(recordEntry.getSystemTime());
// Commit the offset every 1000 records.
if (recordCount % commitNum == 0) {
// Commit the offset.
Map<String, SubscriptionOffset> offsetMap = new HashMap<>();
offsetMap.put(shardId, subscriptionOffset);
datahubClient.commitSubscriptionOffset(projectName, topicName, subId, offsetMap);
System.out.println("commit offset successful");
}
}
cursor = getRecordsResult.getNextCursor();
} catch (SubscriptionOfflineException | SubscriptionSessionInvalidException e) {
// Exit. SubscriptionOfflineException: The subscription is offline. SubscriptionSessionInvalidException: Another client is consuming the same subscription.
e.printStackTrace();
throw e;
} catch (SubscriptionOffsetResetException e) {
// The offset was reset. You need to get the latest version of the SubscriptionOffset.
SubscriptionOffset offset = datahubClient.getSubscriptionOffset(projectName, topicName, subId, shardIds).getOffsets().get(shardId);
subscriptionOffset.setVersionId(offset.getVersionId());
// After an offset is reset, you must get the new cursor. The method you use to get the cursor should match how the offset was reset.
// If both sequence and timestamp were set during the reset, you can get the cursor by using either SEQUENCE or SYSTEM_TIME.
// If only the sequence was set, you must use SEQUENCE.
// If only the timestamp was set, you must use SYSTEM_TIME.
// As a general rule, try to get the cursor using SEQUENCE first, then SYSTEM_TIME. If both fail, use OLDEST.
cursor = null;
if (cursor == null) {
try {
long nextSequence = offset.getSequence() + 1;
cursor = datahubClient.getCursor(projectName, topicName, shardId, CursorType.SEQUENCE, nextSequence).getCursor();
System.out.println("get cursor successful");
} catch (DatahubClientException exception) {
System.out.println("get cursor by SEQUENCE failed, try to get cursor by SYSTEM_TIME");
}
}
if (cursor == null) {
try {
cursor = datahubClient.getCursor(projectName, topicName, shardId, CursorType.SYSTEM_TIME, offset.getTimestamp()).getCursor();
System.out.println("get cursor successful");
} catch (DatahubClientException exception) {
System.out.println("get cursor by SYSTEM_TIME failed, try to get cursor by OLDEST");
}
}
if (cursor == null) {
try {
cursor = datahubClient.getCursor(projectName, topicName, shardId, CursorType.OLDEST).getCursor();
System.out.println("get cursor successful");
} catch (DatahubClientException exception) {
System.out.println("get cursor by OLDEST failed");
System.out.println("get cursor failed!!");
throw e;
}
}
} catch (LimitExceededException e) {
// Limit exceeded, retry.
e.printStackTrace();
retryNum++;
} catch (DatahubClientException e) {
// Other error, retry.
e.printStackTrace();
retryNum++;
} catch (Exception e) {
e.printStackTrace();
System.exit(-1);
}
}
}
Na primeira inicialização, a aplicação começa a consumir dados a partir do registro mais antigo disponível. Durante a execução, atualize a página da assinatura no console web para acompanhar o avanço do offset de consumo do shard.
Caso você altere manualmente o offset usando o recurso Reset Checkpoint no console web enquanto o consumidor está ativo, a aplicação detecta a mudança automaticamente e retoma o consumo a partir do novo offset. Para isso, o cliente captura uma
SubscriptionOffsetResetExceptione chama o métodogetSubscriptionOffsetpara buscar o objetoSubscriptionOffsetmais recente no servidor.Evite utilizar múltiplas threads ou processos de consumo para ler o mesmo shard de uma assinatura simultaneamente. Essa prática faz com que diferentes consumidores sobrescrevam o offset, deixando o valor armazenado em estado indefinido. Nesse cenário, o servidor lança uma
SubscriptionSessionInvalidException. Capture essa exceção, encerre a aplicação e revise seu design para eliminar consumidores duplicados.