En la nota de tormenta: aplicación Trident , hablé del uso de Trident, aquí hablé de los cambios en el estado de Trident y el uso de la API correspondiente.
El contenido de este artículo proviene de Trident State , y parte del contenido se modifica en función de las condiciones reales.
Trident tiene herramientas abstractas de primera clase para leer y escribir datos de estado. El estado se puede almacenar dentro de la topología, como en el contenido y almacenado por HDFS, o almacenado en la base de datos a través de almacenamiento externo (como Memcached o Cassandra). Para la API de Trident, no hay diferencia entre estos dos mecanismos.
Trident administra el estado de manera tolerante a fallas, de modo que las actualizaciones de estado en caso de reintento o falla sean idempotentes. En el procesamiento de big data, la idempotencia del procesamiento de datos es un indicador muy importante, que puede garantizar que incluso si cada mensaje se procesa varias veces, el resultado es como si se procesara solo una vez.
Es posible que se requieran varios niveles de tolerancia a fallas al realizar actualizaciones de estado. Antes de eso, veamos un ejemplo para ilustrar las habilidades necesarias para lograr la semántica "exactamente una vez". Por ejemplo, los datos de la secuencia se cuentan y se agregan, y cada vez que se procesa una nueva tupla, el resultado del recuento en ejecución se almacenará en la base de datos.
Si ocurre una falla, la tupla volverá a ejecutar la operación de conteo. Esto causará problemas al realizar una actualización de estado, porque en este momento no se sabe si se ha actualizado el estado de la tupla. Quizás los datos de la tupla aún no se hayan procesado y el recuento deba aumentarse en este momento. Quizás la tupla se haya procesado y el recuento se haya aumentado correctamente, pero hay un problema en el siguiente paso. En este caso, no se debe aumentar el recuento. También es posible que el procesamiento de tuplas sea normal y el recuento de actualizaciones sea anormal. En este momento, es necesario actualizar el recuento.
Por lo tanto, si simplemente almacena la información del recuento en la base de datos, no sabe si la tupla se ha procesado. Por lo tanto, se necesita más información como ayuda. Trident proporciona las siguientes tres propiedades para lograr un procesamiento "exactamente una vez":
- Las tuplas se procesan en pequeños lotes.
- A cada lote de tuplas se le dará un ID único llamado ID de transacción (txid). Si el lote se procesa repetidamente, el txid será el mismo.
- La operación de actualización de estado se realiza en el orden de lotes de tuplas. En otras palabras, el estado del lote 3 no se actualizará hasta que el estado del lote 2 se actualice correctamente.
De acuerdo con estas características, es posible verificar si el lote de la tupla ha sido procesado y tomar las acciones adecuadas para actualizar el estado de acuerdo con el resultado de la detección. La acción específica que se tome depende del tipo de boquilla. Hay tres tipos de Spout: "no transaccional", "transaccional" y "transaccional opaco". La tolerancia a fallos correspondiente también es de tres tipos: "no transaccional", "transacción" y "transacción opaca". Echemos un vistazo a los distintos tipos de Spout y la tolerancia a fallas correspondiente.
Spout transaccional
Trident envía tuplas en lotes para su procesamiento, y cada lote de tuplas recibe un ID de transacción único. Las características de Spout se determinan en función del mecanismo de garantía de tolerancia a fallos que proporcionan, y este mecanismo también afectará a cada lote. Transactional Spout tiene las siguientes características:
- El txid de cada lote no cambia. Para un txid específico, cuando se repite la ejecución, los datos de tupla que contiene son exactamente los mismos que la primera vez.
- Las tuplas solo aparecerán en un lote y no se repetirán (una tupla solo aparecerá en un lote y no aparecerá en varios lotes).
- Cada tupla aparecerá una vez (no se perderán datos de tupla)
Este es el tipo Spout más simple y fácil de entender. El flujo de datos se divide en lotes fijos. Hay una extensión de Spout transaccional integrado con Kafka en Storm, y el código está aquí .
Dado que Spout transaccional es tan simple y fácil de entender, ¿por qué no utilizar Spout transaccional completamente en Trident? De hecho, radica en su tolerancia a fallos. Por ejemplo, TransactionalTridentKafkaSpout
el método de trabajo es que el lote del mismo txid contendrá las tuplas de todas las particiones de Kafka. Una vez que se emite un lote, se produce una excepción y debe volver a emitirse, y se requiere exactamente el mismo conjunto de tuplas para cumplir con la semántica requerida por Spout transaccional. Pero en este momento, si un cierto nodo en Kafka es anormal (el nodo está inactivo o la partición no está disponible), no se puede obtener exactamente el mismo lote de tuplas, y toda la topología debe detenerse para la tercera semántica (ejecución por lotes en secuencia).
Esta es la razón del Spout de "tipo de transacción opaca", que puede tolerar la pérdida del nodo de la fuente de datos y puede garantizar que los datos se utilicen exactamente una vez.
Nota: Aquellos que estén familiarizados con Kafka deben pensar que si un tema admite la replicación, incluso si un nodo no está disponible, habrá otros nodos de replicación en la parte superior, entonces TransactionalTridentKafkaSpout también puede evitar los problemas anteriores.
Sigamos para ver cómo diseñar una implementación de estado que admita la semántica de Spout "transaccional" (en resumen, los datos de tupla por lotes correspondientes al mismo txid son exactamente los mismos) que admita exactamente una característica. Este estado se llama "estado transaccional".
Por ejemplo, ahora hay una topología de recuento de palabras y el recuento de palabras debe almacenarse en la base de datos de clave / valor. La clave es una palabra y el valor contiene el número de palabras. Además, para determinar si se ha ejecutado el mismo lote de tuplas, el txid debe almacenarse en el valor. De esta forma, cuando sea necesario actualizar el número de palabras, primero compare si el txid es el mismo y, si son iguales, omita la actualización. Si son diferentes, actualice el recuento.
Considere este ejemplo de por qué funciona. Suponga que está procesando txid 3 que consta de las siguientes tuplas de lote:
Por ejemplo, para procesar un lote de tuplas cuyo txid es 3:
["man"]
["man"]
["dog"]
Los datos almacenados actualmente en la base de datos son:
man => [count=3, txid=1]
dog => [count=4, txid=3]
apple => [count=10, txid=2]
En este momento, se encuentra que el txid correspondiente a "man" es 1, y el txid actual es 3, y se puede actualizar. Entonces, el txid correspondiente a "perro" es 3, lo que indica que se ha enviado el mismo lote de datos de tupla y no es necesario actualizarlo. A partir de este punto, se puede ver que las tuplas de lote con un txid de 3. Después de actualizar el número de "perro" y antes de actualizar el número de "hombre", ocurrió un error. El resultado final es:
man => [count=5, txid=3]
dog => [count=4, txid=3]
apple => [count=10, txid=2]
Pico transaccional opaco
Como se mencionó anteriormente, el Spout transaccional opaco no puede garantizar que los datos de tupla en el lote correspondiente al mismo txid sean completamente consistentes. Sus características son las siguientes:
- Cada tupla se procesará correctamente en un solo lote.
[OpaqueTridentKafkaSpout](http://github.com/apache/storm/tree/v1.1.0/external/storm-kafka/src/jvm/org/apache/storm/kafka/trident/OpaqueTridentKafkaSpout.java)
Con esta función, también es muy tolerante a las anomalías del nodo kafka. OpaqueTridentKafkaSpout
Al enviar un lote de tuplas, se enviará desde la posición posterior al último éxito, para garantizar que las tuplas no se pierdan ni se retransmitan.
Con base en las características anteriores, el Spout transaccional opaco es diferente de txid para determinar directamente si se puede omitir la actualización de estado, porque las tuplas en los lotes con el mismo txid pueden haber cambiado.
Esto requiere almacenar más información de estado, no solo un resultado y un txid, sino también el valor del resultado anterior.
Por ejemplo, el recuento de lotes actual es 2 y se requiere una actualización de estado. Los datos en la base de datos son los siguientes:
{
"value": 4,
"prevValue": 1,
"txid": 2
}
Si el txid actual es 3, es diferente de la base de datos. En este caso, el prevValue
valor es el value
valor value
del valor aumentado de 2, se actualiza txid
a 3, el resultado final es:
{
"value": 6,
"prevValue": 4,
"txid": 3
}
Si el txid actual es 2, es igual al txid en la base de datos. Debido a que el txid es el mismo, significa que el procesamiento por lotes con el txid de 2 falló la última vez, pero la tupla de este tiempo puede ser diferente de la última vez. En este momento, es necesario utilizar estos datos para sobrescribir el último resultado de procesamiento. Es decir, el prevValue
valor no cambia, value
el valor se cambia para prevValue
aumentar en 2, txid
sin cambios, y el resultado final es el siguiente:
{
"value": 3,
"prevValue": 1,
"txid": 2
}
La viabilidad de este enfoque depende de la fuerte secuencia de Trident. En otras palabras, una vez que se procesa un nuevo lote, el lote anterior no se repetirá. El Spout transaccional opaco asegura que no haya duplicación entre diferentes lotes, es decir, cada tupla solo se procesará con éxito en un lote, por lo que puede usar de manera segura el valor anterior y el valor actual para sobrescribir los datos existentes.
Spout no transaccional
Spout no transaccional no puede ofrecer ninguna garantía para los lotes. Por lo tanto, puede haber un procesamiento "al menos una vez", es decir, falla durante un procesamiento por lotes, pero no se volverá a procesar; también puede proporcionar un procesamiento "al menos una vez", es decir, puede haber varios lotes para procesar. un determinado lote por separado Tuplas. Es decir, no hay forma de lograr la semántica de "exactamente una vez".
Resumen de diferentes tipos de picos y estados
Lo siguiente es si diferentes combinaciones de canalización / estado admiten la semántica de procesamiento "exactamente una vez":
El estado de transacción opaco tiene la tolerancia a fallas más fuerte, pero debido a que almacenar txid y dos resultados trae una mayor sobrecarga. El estado transaccional solo necesita almacenar un resultado de estado, pero solo es válido para Spout transaccional. El estado no transaccional requiere que se almacenen menos datos, pero no puede lograr la semántica de procesamiento "exactamente una vez".
Por lo tanto, al elegir la tolerancia a fallas y el espacio de almacenamiento, debe elegir una combinación adecuada de acuerdo con sus necesidades específicas.
API de estado
Desde el punto de vista anterior, el principio de la semántica "exactamente una vez" es un poco complicado, pero como usuario, no es necesario que comprenda estas comparaciones txid y el almacenamiento de valores múltiples, porque Trident ha encapsulado todos los valores tolerantes a fallas. procesamiento de la lógica en el estado, solo hay que pensar en ello y llevar el código servirá:
TridentTopology topology = new TridentTopology();
TridentState wordCounts =
topology.newStream("spout1", spout)
.each(new Fields("sentence"), new Split(), new Fields("word"))
.groupBy(new Fields("word"))
.persistentAggregate(MemcachedState.opaque(serverLocations), new Count(), new Fields("count"))
.parallelismHint(6);
En él se ha encapsulado toda la lógica de estado de transacción opaca MemcachedState.opaque
. Además, las actualizaciones de estado se ajustarán automáticamente a las operaciones por lotes, lo que puede reducir el desperdicio de recursos causado por interacciones repetidas con la base de datos.
La State
interfaz básica tiene solo dos métodos:
public interface State {
void beginCommit(Long txid); // 对于像DRPC流发生的partitionPersist这样的事情,可以是null
void commit(Long txid);
}
Como se mencionó anteriormente, el txid se obtendrá al principio y al final de la actualización de estado. A Trident no le importa cómo se opera el estado, qué método se usa para actualizar y qué método se usa para leer.
Si tiene una base de datos personalizada que contiene información sobre la dirección del usuario, debe usar Trident para interactuar con la base de datos. La State
clase extendida contiene métodos getter y setter para la información del usuario:
public class LocationDB implements State {
public void beginCommit(Long txid) {
}
public void commit(Long txid) {
}
public void setLocation(long userId, String location) {
// 向数据库设置地址信息
}
public String getLocation(long userId) {
// 从数据库中获取地址信息
}
}
Entonces necesitas uno StateFactory
para crear los State
objetos que LocationDB
necesita Trident . La StateFactory
estructura general requerida es la siguiente:
public class LocationDBFactory implements StateFactory {
public State makeState(Map conf, int partitionIndex, int numPartitions) {
return new LocationDB();
}
}
Trident proporciona una QueryFunction
interfaz para consultar la fuente de estado y una interfaz para actualizar la fuente de estado StateUpdater
. Por ejemplo, la LocationDB
información del usuario en la consulta QueryLocation
:
TridentTopology topology = new TridentTopology();
TridentState locations = topology.newStaticState(new LocationDBFactory());
topology.newStream("myspout", spout)
.stateQuery(locations, new Fields("userid"), new QueryLocation(), new Fields("location"));
QueryLocation
El código es el siguiente:
public class QueryLocation extends BaseQueryFunction<LocationDB, String> {
public List<String> batchRetrieve(LocationDB state, List<TridentTuple> inputs) {
List<String> ret = new ArrayList();
for (TridentTuple input : inputs) {
ret.add(state.getLocation(input.getLong(0)));
}
return ret;
}
public void execute(TridentTuple tuple, String location, TridentCollector collector) {
collector.emit(new Values(location));
}
}
QueryFunction
La operación se divide en dos pasos: Primero, Trident coloca los datos recopilados en un lote y los envía al batchRetrieve
método. En este ejemplo, el batchRetrieve
método recibió alguna identificación de usuario. batchRetrieve
Devolverá un conjunto de resultados con la misma longitud que la tupla de entrada. Los elementos de la tupla de entrada y el resultado de salida se corresponden entre sí.
Desde este punto de vista, la LocationDB
clase anterior no aprovecha el procesamiento por lotes de Trident, por lo que debe modificarse con todos los esfuerzos:
public class LocationDB implements State {
public void beginCommit(Long txid) {
}
public void commit(Long txid) {
}
public void setLocationsBulk(List<Long> userIds, List<String> locations) {
// set locations in bulk
}
public List<String> bulkGetLocations(List<Long> userIds) {
// get locations in bulk
}
}
Las QueryLocation
clases correspondientes son las siguientes:
public class QueryLocation extends BaseQueryFunction<LocationDB, String> {
public List<String> batchRetrieve(LocationDB state, List<TridentTuple> inputs) {
List<Long> userIds = new ArrayList<Long>();
for (TridentTuple input : inputs) {
userIds.add(input.getLong(0));
}
return state.bulkGetLocations(userIds);
}
public void execute(TridentTuple tuple, String location, TridentCollector collector) {
collector.emit(new Values(location));
}
}
Este código reduce en gran medida las operaciones de la base de datos.
Para el estado de actualización, se puede utilizar la StateUpdater
interfaz. Por ejemplo, la siguiente operación de actualización:
public class LocationUpdater extends BaseStateUpdater<LocationDB> {
public void updateState(LocationDB state, List<TridentTuple> tuples, TridentCollector collector) {
List<Long> ids = new ArrayList<Long>();
List<String> locations = new ArrayList<String>();
for (TridentTuple t : tuples) {
ids.add(t.getLong(0));
locations.add(t.getString(1));
}
state.setLocationsBulk(ids, locations);
}
}
La topología de la operación de actualización correspondiente puede ser así:
TridentTopology topology = new TridentTopology();
TridentState locations =
topology.newStream("locations", locationsSpout)
.partitionPersist(new LocationDBFactory(), new Fields("userid", "location"), new LocationUpdater());
partitionPersist
El método actualiza el estado y la StateUpdater
interfaz recibe un lote de tuplas e información de estado, y luego actualiza el estado. La LocationUpdater
clase anterior simplemente toma la identificación del usuario y la información de la dirección de la tupla, y luego realiza el procesamiento por lotes en el estado. Luego, partitionPersist
se devolverá un TridentState
objeto que representa el estado actualizado . Luego, puede usar el stateQuery
método para consultar el estado en cualquier otro lugar de la topología .
Hay un parámetro en StateUpdater
el updateState
método TridentCollector
, este objeto puede enviar las tuplas enviadas a un nuevo flujo de datos. No se utiliza en este ejemplo. Si necesita realizar operaciones posteriores, como actualizar el valor de recuento en la base de datos, puede TridentState#newValuesStream
obtener nuevos datos de flujo de datos a través de métodos.
persistentAggregate
Trident usa un persistentAggregate
método llamado estado de actualización. Ha aparecido antes, así que escríbalo de nuevo aquí:
TridentTopology topology = new TridentTopology();
TridentState wordCounts =
topology.newStream("spout1", spout)
.each(new Fields("sentence"), new Split(), new Fields("word"))
.groupBy(new Fields("word"))
.persistentAggregate(new MemoryMapState.Factory(), new Count(), new Fields("count"));
partitionPersist
Es un método que recibe el agregador Trident como parámetro y actualiza los datos de estado, persistentAggregate
es partitionPersist
una abstracción de programación construida en la capa superior. En este ejemplo, al groupBy
devolver un paquete de datos, Trident necesita un MapState
objeto que implemente la interfaz. El campo de agrupación es la clave del estado y el resultado de la agregación es el valor del estado. MapState
La interfaz es la siguiente:
public interface MapState<T> extends State {
List<T> multiGet(List<List<Object>> keys);
List<T> multiUpdate(List<List<Object>> keys, List<ValueUpdater> updaters);
void multiPut(List<List<Object>> keys, List<T> vals);
}
Si necesita realizar operaciones de agregación en flujos de datos no agrupados, Trident necesita un Snapshottable
objeto que implemente la interfaz:
public interface Snapshottable<T> extends State {
T get();
T update(ValueUpdater updater);
void set(T o);
}
Tanto MemoryMapState como MemcachedState implementan estas interfaces.
Implementar la interfaz MapState
Implementar la MapState
interfaz es muy simple, Trident ha hecho casi todo. OpaqueMap
, TransactionalMap
E NonTransactionalMap
implementar su propia semántica tolerante a fallas respectivamente. Solo es necesario proporcionar una implementación para la adquisición y modificación por lotes de diferentes claves / valores para estas clases IBackingMap
. IBackingMap
La interfaz es la siguiente:
public interface IBackingMap<T> {
List<T> multiGet(List<List<Object>> keys);
void multiPut(List<List<Object>> keys, List<T> vals);
}
OpaqueMap
El método se llamará utilizando OpaqueValue como parámetro vals multiPut
; TransactionalValueTransactionalMap
se utilizará como parámetro; y el objeto de topología se pasará directamente.NonTransactionalMaps
Trident también proporciona la clase CachedMap para realizar la operación automática de almacenamiento en caché de LRU de clave / valor.
Finalmente, Trident también proporciona la clase SnapshottableMap , que convierte MapState
objetos en Snapshottable
objetos almacenando los resultados de la agregación global en una clave fija .
Puede consultar la implementación de MemcachedState para aprender cómo combinar estas herramientas para proporcionar una MapState
implementación de alto rendimiento . MemcachedState
Admite semánticas opacas de transacciones, transacciones y no transacciones.
Página de inicio personal: http://www.howardliu.cn
Publicación de blog personal: notas de tormenta: estado de tridente
Página de inicio de CSDN: http://blog.csdn.net/liuxinghao
Publicación de blog de CSDN: notas de tormenta: estado de tridente