Suche
Asynchrone Kommunikation zwischen Anwendungen

ActiveMQ in der Praxis

Ivan Mioc

Fast jede IT-Firma hat mittlerweile in mindestens einem Projekt ActiveMQ eingesetzt. Google-Trends geben darüber hinaus Anlass zu der Vermutung, dass es die meistgenutzte Message-oriented Middleware (MOM) sein könnte. Hauptsächlich in kleineren, unkomplizierten Anwendungslandschaften eingesetzt, zeichnet sich ActiveMQ durch eine unkomplizierte Handhabung, einfache Konfiguration und sehr gute Performance aus. In diesem Artikel bekommen Sie einen Überblick über Funktionsweise und Einsatz von ActiveMQ.

ActiveMQ wird entweder als JMS Provider oder als Broker bezeichnet und hat damit die Rolle eines Servers. Demgegenüber sind alle anderen Anwendungen im JMS-Netzwerk dagegen JMS-Clients. Ein Broker kennt zwei Arten von Destinationen: Queue und Topic. Sobald der Broker die erste Nachricht für diese Destination empfangen hat, wird in beiden Fällen die Destination samt ihrer Art automatisch festgestellt. Queues kennen zwei Arten von Clients: Producer und Consumer. Hat ein Consumer eine Nachricht von einem Producer empfangen, wird diese Nachricht aus der Queue gelöscht und steht den anderen Consumern nicht mehr zur Verfügung. Eine Nachricht kann also nur von einem einzelnen Consumer empfangen (konsumiert) werden.

Voraussetzungen und Hintergrundinformationen

Es wird vorausgesetzt, dass Sie schon mit ActiveMQ in Berührung gekommen sind. Wenn nicht, empfiehlt sich vor dem Lesen dieses Artikels: Für grundlegende Informationen über JMS ist die Definition in Wikipedia lesenswert und für weitere JMS-Details dieses Buch. Unter diesem Link kann man ActiveMQ herunterladen. Für die Installation und einen ersten Start empfiehlt es sich, das richtige „Getting Started“ zu lesen. Danach kann man das Beispiel zum Laufen bringen. Dabei ist es sehr interessant, mittels der Web Console unter http://localhost:8161/admin/queues.jsp den ActiveMQ zu beobachten.

In diesem Artikel wird auch die eine oder andere weitergehende Frage angesprochen, die vielleicht eine tiefere Auseinandersetzung mit der Materie erforderlich macht. Dazu empfehlen sich das ActiveMQ Buch, die Internetpräsenz von ActiveMQ und Fuse Message Broker.

Topics kennen ebenfalls zwei Arten von Clients: Publisher und Subscriber. In diesem Fall ist es jedoch möglich, dass mehrere Subscriber die gleiche Nachricht empfangen.

Im Folgenden gehen wir auf das Queue-Verhalten von ActiveMQ ein. Nach dem KISS-Prinzip hätte eine Queue im Idealfall einen oder mehrere Producer und nur einen Consumer. Oft ist es jedoch erforderlich, entweder aus historischen Gründen oder wegen der Nutzung von ActiveMQ für synchrone Kommunikation, mehrere Consumer einzusetzen. In diesem Fall besteht theoretisch die Gefahr, dass ein Consumer einem anderem die Nachricht wegschnappt. Aus diesem Grund nutzen alle Consumer unterschiedliche Selektoren, um die eigenen Nachrichten herauszufiltern. Mit „Producer“ und „Consumer“ sind in diesem Artikel die Producer- bzw. die Consumer-Anwendung ohne Connection Pool gemeint. In vielen Fällen tritt eine Anwendung sogar in beiden Funktionen auf. Das kann sogar für einen Thread gelten, der ebenfalls gleichzeitig Consumer und Producer sein kann. Hier spricht man über „synchrone Kommunikation“, und in einem solchen Fall werden die oben erwähnten Selektoren genutzt.

Konfiguration und Verbindungsmöglichkeiten

Die Datei activemq.xml ist in den meisten Fällen die einzige Datei, die geändert werden muss, um eine gewünschte Konfiguration zu erzielen. Activemq.xml und alle anderen zu bearbeitenden Dateien befinden sich im /conf-Verzeichnis. Innerhalb des activeMQ.xml befindet sich auch die Transport-Connector-Konfiguration, z. B.:

	<transportConnectors>
	<transportConnector name="openwire" uri="tcp://0.0.0.0:61616"/>
	<transportConnector name="ow2" uri="tcp://0.0.0.0:61617"/>
	<transportConnector name="stomp" uri="stomp://0.0.0.0:61613"/>
	</transportConnectors>

Das ist eine typische Konfiguration, die in der Praxis für Standalone ActiveMQ in anspruchsvolleren Systemen vorkommen könnte. Es ist üblich, mehrere Ports für das TCP-Protokoll zu öffnen und einen Port für das STOMP-Protokoll, denn JMS-Clients verbinden sich über TCP-Ports. Um nicht zu viele Clientverbindungen über einen einzelnen Port laufen zu lassen, definiert man standardmäßig mehrere TCP-Ports. Bei ActiveMQ ist openWire das Standardprotokoll für die JMS-Clients. Wenn in einem URI „TCP“ steht, bedeutet das folglich „openWire über TCP“. Ein JMS-Client-Entwickler muss sich keine Gedanken über das openWire-Protokoll machen. Es genügt, wenn activemq-rar.rar bei dem Client deployt ist. Für Clients, geschrieben in C#, C und C++, gibt es Bibliotheken für das openWire-Transport-Protokoll, was die Clientprogrammierung möglich macht, aber nicht unbedingt einfach. Eine gängige Alternative für Non-Java-Clients ist das STOMP-Protokoll, ein sehr einfaches Protokoll, das man sehr gern mithilfe von Telnet für Ping-Tests nutzt. Aus diesen zwei Gründen (Einfachheit, Ping-Tests) ist es empfehlenswert, das STOMP-Protokoll immer zu konfigurieren. Obwohl mit openWire und STOMP grundsätzlich zufriedenstellende Ergebnisse erzielt werden, besteht manchmal Bedarf nach Alternativen. Für anspruchsvolle Entwickler hier ein weiteres Beispiel:

	<transportConnectors>
	<transportConnector name="nio" uri="nio://0.0.0.0:61616"/>
	<transportConnector name="udp" uri="udp://0.0.0.0:61617"/>
	<transportConnector name="ssl" uri="ssl://0.0.0.0:61618"/>
	<transportConnector name="http" uri="http://0.0.0.0:61619)"/>
	</transportConnectors>

Der NIO Transport Connector basiert auf dem New-I/O API. Bei sehr vielen Clients oder zu hohem Load für TCP bzw. openWire ist ein Versuch mit NIO eventuell Erfolg versprechend. Eine andere Möglichkeit, die Performance zu verbessern, ist der Einsatz des UDP-Protokolls. Das ist aber nur sinnvoll, wenn Zuverlässigkeit keine so große Rolle spielt. Man erzielt zwar fast immer bessere Ergebnisse, aber in einzelnen Fällen könnten Nachrichten verloren gehen.

Wer höhere Anforderungen bezüglich der Sicherheit hat, kann das SSL-(Secure-Socket-Layer-)Protokoll nutzen. Das SSL-Protokoll basiert genauso wie NIO auf TCP. Es ist lediglich zu beachten, wie üblich eigene private/public Keys bzw. Zertifikate zu generieren und die vorhandenen Standarddateien (*.ts, *.ks *.cert) aus dem /conf-Verzeichnis zu löschen. Während NIO immer zu einer Performanceverbesserung führt, erzeugt SSL aufgrund der ständigen Kodierung/Dekodierung immer eine Performanceverschlechterung. Unter welchen Bedingungen lohnt es sich, HTTP bzw. HTTPS zu verwenden? Nur dann, wenn es sich nicht vermeiden lässt, z. B., wenn eine Firewall sehr restriktiv eingestellt ist und nur das HTTP-Protokoll erlaubt.

ActiveMQ bietet neben STOMP auch das REST- und das Ajax-API. Beide APIs lassen sich mithilfe der schon vorhandenen Webserver (Jetty) nutzen. Jetty startet standardmäßig mit ActiveMQ und es ist nur noch erforderlich, mithilfe der Datei web.xml die Servlets entsprechend zu konfigurieren. Falls Sie über den Einsatz des REST-APIs nachdenken, sollten Sie in Betracht ziehen, dass STOMP mit großer Wahrscheinlichkeit für die gleichen Zwecke besser geeignet ist. Dessen Einfachheit und die einfache ActiveMQ-Implementierung sind starke Argumente für STOMP. Die Verwendung des Ajax-APIs könnte dagegen sehr interessant für die Realtime-Darstellung schnell veränderlicher Daten sein.

Clientkonfiguration

Nachdem der JMS-Provider (Broker) konfiguriert ist, muss nun auch der JMS-Client konfiguriert werden. Bei der Clientanwendung muss die Datei activema-rar.rar deployt werden. Die einfachste Konfiguration besteht darin, bei JBoss die Datei ra.xml innerhalb activemq-rar.rar und unterhalb der META-INF zu definieren bzw. ändern:

	<config-property>
	...
	<config-property-value>tcp://firstB:61616</config-property-value>
	</config-property>

Andere Applikationsserver nutzen anstatt ra.xml unter Umständen andere Konfigurationsdateien oder -verzeichnisse. Manche unterstützen auf sehr einfache Art und Weise standardmäßig mehrere Connection Strings. In diesem Fall kann der Client auch unterschiedliche Protokolle verwenden und sich mit unterschiedlichen Gruppen von Brokern verbinden. Im obigen Beispiel mitra.xml (eigentlich standardmäßig bei dem JBoss) ist es allerdings nur möglich, einen einzelnen Connection String zu definieren. Das bedeutet, dass aus der Sicht des JMS-Clients alle Queues gleich konfiguriert sind.

Nachrichten-Acknowledge, Synchronous send/receive

Wie schon eingangs erwähnt, existieren bei ActiveMQ drei Rollen: Producer, Broker und Consumer. Auch ist es klar geworden, dass es zwei Nachrichtenwege gibt: Vom Producer zum Broker und vom Broker zum Consumer. Wenn Sie sicher sein möchten, dass die gesendete Nachricht angekommen ist, sollten Sie die Auslieferung per Acknowledgement bestätigen lassen. Ist die Performance wichtiger als garantierte Auslieferung, können Sie auf die Bestätigung verzichten. Im Folgenden bedeutet der Begriff „Synchronous send/receive“ nichts anderes, als dass Nachrichten bestätigt werden müssen.

Wie sieht es im Nachrichtenweg vom Producer zum Broker aus? Standardmäßig sendet der Producer die permanenten Nachrichten synchron und wartet auf die Bestätigung (Acknowledge) vom Broker. Der Producer lässt sich auch auf nichtpermanente Nachrichten umprogrammieren, oder Sie können bei permanenten Nachrichten bleiben und nur den Sendemodus auf asynchron umprogrammieren. Beides bedeutet allerdings automatisch, dass Nachrichten asynchron gesendet werden und der Producer keine Bestätigung erwartet.

Wie sieht es dann mit der Verlässlichkeit aus? Es ist möglich, im Failover-Protokoll parameters, trackMessages und maxCacheSize zu definieren, was im Fall von Netzwerkproblemen weiterhilft. Nach einem Netzwerkabsturz werden alle Nachrichten aus dem Cache erneut gesendet. Gegen den Absturz des ActiveMQ Brokers hilft dagegen nur die Nutzung permanenter Nachrichten. Sie werden vor der weiteren Verarbeitung in einer Datenbank oder in Dateien auf die Festplatte geschrieben. Auf dem Nachrichtenweg vom Broker zum Consumer schickt der Broker die Nachrichten standardmäßig synchron und erwartet ein Acknowledge. Das ist allerdings nicht zu verwechseln mit dem „Asynchronous Dispatch“ – was leider häufig passiert. „Asynchronous Dispatch“ bezieht sich lediglich auf die Consumer Session Internal Queue.

Beim Consumer unterscheidet man zwischen einem synchronen Consumer mit der Methode receive()und einem asynchronen Consumer mit dem MessageListener. Der asynchrone Consumer hat eine deutlich bessere Performance und bringt keine Nachteile. Es stellt sich lediglich die Frage, ob er für die Consumer-Anwendung geeignet ist. Beide Arten von Consumern müssen die Nachrichten bestätigen. Das Einzige, was Sie ändern können, ist, nicht jede einzelne Nachricht zu bestätigen (standardmäßiges Verhalten), sondern eine Bestätigung für mehrere Nachrichten zu senden (ändern Sie den Session Acknowledge Mode). Dabei gibt es verschiedene Optionen: Allen ist gemeinsam, dass sie einerseits eine bessere Performance erzielen. Dem gegenüber steht andererseits aber die Gefahr, dass doppelt gesendete Nachrichten auftreten können.

Prefetch Size ist ein weiterer sehr wichtiger Parameter, den Sie für jeden Consumer definieren sollten. Dieser Parameter definiert, wie viele nicht bestätigte Nachrichten der Consumer erhalten darf. Es gilt grundsätzlich: Je größer der Parameter Prefetch Size ist, desto besser ist die Performance. Dabei dürfen Sie aber nicht die Größe des Arbeitsspeichers und andere Besonderheiten des Consumers aus den Augen verlieren. Die Nachrichten werden standardmäßig vom Broker gepusht. Nur wenn Prefetch Size „0“ ist, werden die Nachrichten vom Consumer gepullt.

Wie im vorherigen Abschnitt schon erwähnt, tritt eine Clientanwendung oft gleichzeitig als Producer und Consumer auf. In den meisten Fällen lassen sich beide getrennt betrachten, aber in einem Request-Reply- oder einem Reply-to-Szenario ist die getrennte Betrachtung nicht leicht erreichbar. Beide Bezeichnungen beziehen sich auf dasselbe Szenario: Der Producer sendet eine Nachricht zum Consumer und erwartet eine Antwort. Der Entwickler einer JMS-Client-Anwendung könnte das oben genannte Szenario so implementieren, dass dieselbe Methode, die eine Nachricht gesendet hat, synchron auf die Reply-to-Nachricht an einem nur dafür erzeugten vorübergehenden Queue wartet. Das ist dann eine Vermischung vom Producer und vom Consumer in derselben Methode und sollte vermieden werden.

Eine saubere Lösung kann durch die Nutzung von JMSCorrelationId und die Definition einer Standard-Reply-to-Queue (im Gegensatz zu oben erwähnter vorübergehender Queue) in jeder Nachricht erreicht werden. So muss die Anwendung nicht mehr den Producer und den Consumer in derselben Methode haben, und die Anwendung kann die Consumer in einer anderen Methode (Klasse) implementieren. Diese Methode wartet entsprechend auf der bestimmten (reply-to) Queue auf die Nachrichten mit einer bestimmten JMSCorrelationId.

Persistente Nachrichten

ActiveMQ bietet drei Möglichkeiten, Nachrichten zu speichern. Zwei davon sind ActiveMQ-eigene Data Stores, basierend auf der journalartigen Speicherung der Nachrichten in den Dateien: KahaDB und AMQ Message Store. Die dritte Möglichkeit besteht darin, die Speicherung an eine relationale Datenbank zu knüpfen. Als erste Wahl empfiehlt sich AMQ Message Store, der die beste Performance bietet und sich schon seit Jahren bewährt hat. Hier ein Konfigurationsbeispiel (aus activemq.xml):

	<persistenceAdapter>
	<amqPersistenceAdapter
	directory="${activemq.base}/data/amq-ds"
	indexBinSize="2048"
	indexPageSize="32kb"
	archiveDataLogs="false"
	syncOnWrite="true"/>
	</persistenceAdapter>

Damit ist die Konfiguration abgeschlossen. Weitere Details zur Konfiguration können Sie hier nachlesen.

Ab 1000 Queues pro Broker ist KahaDB eine Erwägung wert. KahaDB leidet vielleicht noch immer unter Kinderkrankheiten, braucht aber deutlich weniger Speicherplatz und bietet bessere Performance bei einer hohen Anzahl von Queues/Clients. Ab welcher Anzahl sich der Einsatz wirklich lohnt, ist allerdings von Fall zu Fall unterschiedlich und Erfahrungssache. An dieser Stelle wird ein Beispiel für die Konfiguration von KahaDB in config.xml dargestellt:

	<persistenceAdapter>
	<kahaDB
	directory="${activemq.base}/data/kahadb"
	journalMaxFileLength="16mb" />
	</persistenceAdapter>

Wenn man eine Shared Data Storage braucht (z. B. für eine Master/Slave-Topologie), besteht kaum eine andere Alternative, als eine relationale Datenbank zu nutzen. Die ist deutlich langsamer als KahaDB oder AMQ Data Store, aber das ist der Preis für die geforderte Redundanz. Die genaueren Konfigurationsdetails können Sie hier finden. Der Persistence Adapter wird ähnlich wie im obigen Beispiel definiert, braucht aber zusätzlich eine Spring-Bean-Definition. Es ist wichtig zu beachten, dass non-journaled Store benötigt wird und definiert werden sollte.

Mehrere Broker

Bisher haben wir nur Standalone Broker betrachtet. Wir haben Konfigurationsänderungen für die Verbindungen (Protokolle) und für die Speicherung der persistenten Nachrichten vorgenommen. Diese Einstellungen werden sich nicht ändern, wenn wir uns jetzt mit mehreren Brokern beschäftigen. Allerdings, wenn mehr als ein Broker vorhanden ist, stellt sich die Frage, wie ein Client mit mehreren Brokern kommuniziert, oder wie die Broker untereinander kommunizieren. ActiveMQ bietet dazu mehrere Protokolle, von denen Sie im Folgenden die am häufigsten genutzten finden. Auf der Clientseite wird meistens ein Failover-Protokoll in den Connection String eingesetzt. Ein Beispiel aus der Datei ra.xml innerhalb activemq-rar.rar unterhalb META-INF folgt:

	<config-property>
	…
	<config-property-value>
	failover:(tcp://firstB:61616,tcp://secondB:61616)
	</config-property-value>
	</config-property>

Das bedeutet: Der Client verbindet sich mit einem beliebigen Broker. Wenn dieser Broker oder die Netzwerkverbindung ausfällt, verbindet sich der Client automatisch mit einem anderen Broker. Für die Verbindungen zwischen den Brokern ist das Static-Protokoll geeignet:

	<networkConnectors>
	<networkConnector
	name="firstB"
	uri="static:(tcp://secondB:61619)"
	networkTTL="2"
	duplex="false">
	</networkConnector>
	</networkConnectors>

Wie man in obigem Beispiel erkennen kann, ist die Verwendung eines Static- und Failover-Protokolls auch dann sinnvoll, wenn es nur eine Verbindung gibt. Beide Protokolle sorgen dafür, dass sich Clients neu verbinden, wenn die Netzwerkverbindung getrennt wurde.

Einfaches Zwei-Broker-Netzwerk

Wenn man über mehrere Broker spricht, sind in der Regel nur zwei gemeint. Schauen wir uns solch ein Paar noch einmal im Detail an. Eigentlich kommt diese Konfiguration oft als Antwort auf die Frage: Aber was passiert, wenn der Broker ausfällt? Die erste Möglichkeit für Notfälle wäre, einen Broker dazu zu stellen und jeden Client mit randomise=false zu konfigurieren: failover:(tcp://firstB:61616,tcp://secondB:61616)?randomise=false.

Ein so konfigurierter Client versucht zuerst, sich mit dem ersten Broker zu verbinden. Erst wenn das nicht gelingt, oder wenn der erste Broker ausfällt, verbindet er sich mit dem zweiten Broker. Im Normalfall sind also alle Clients mit dem ersten Broker verbunden. Bei dessen Ausfall wechseln alle Clients automatisch zum zweiten Broker. Ist der erste Broker wieder erreichbar, bleiben trotzdem alle reinen Consumer mit dem zweiten Broker verbunden. Producer (bzw. gemischte Clients) verbinden sich dagegen immer wieder neu und wechseln dadurch schnell zum ersten Broker zurück. Damit entsteht ein Mischmasch aus Brokerverbindungen, und dagegen hilft am besten ein Neustart des zweiten Brokers. Danach besteht wieder das ursprüngliche Failover-Netzwerk. Diese Clientkonfiguration kann trotzdem sehr hilfreich sein, insbesondere für Testzwecke.

Wenn nur Non-Persistente-Nachrichten vorliegen würden, könnte dieses Netzwerk ohne Verbindung zwischen den Brokern bzw. ohne Network Connectors gut funktionieren. Aber sobald persistente und stehengebliebene Nachrichten im ersten Broker bzw. dementsprechend in KahaDB oder AMQ Data Store gespeicherte Nachrichten existieren, wird der Network Connector benötigt. Nachdem der erste Broker wieder erreichbar ist, ermöglicht der Network Connector, dass die stehengebliebenen Nachrichten vom ersten Broker zu den Clients am zweiten Broker gehen. Was wir eben beschrieben haben, könnte man ein Store-und-Forward-Netzwerk mit 100 Prozent Failover ohne gemeinsamen Datenspeicher nennen. Dabei könnte es zwei weitere Probleme geben: Die permanenten Nachrichten könnten länger am nichterreichbaren Broker stehenbleiben, und die 100-Prozent-Failover-Konfiguration des Clients nutzt die Ressourcen nicht optimal.

Um das zweite Problem zu lösen, kann man einfach randomise=true stehen lassen (Standardverhalten). Das erste Problem lässt sich dagegen durch die Nutzung der gemeinsamen relationalen Datenbank lösen. Dafür wird nicht das Store-and-Forward-Netzwerk (mit Network-Connectors definiert an den beiden Brokern) genutzt, sondern eine Master/Slave-Konfiguration. Dazu ist es nur erforderlich, beim ActiveMQ Slave Broker den MasterConnector zu definieren (in activemq.xml). Ein Beispiel für die Definition eines MasterConnectors:

Dabei bleiben die Clients gleich konfiguriert wie vorher (randomise=false). Dieses Netzwerk könnte man „Master/Slave Netzwerk mit 100 Prozent Failover und gemeinsamer relationaler Datenbank“ nennen. Eigentlich kann man nichts falsch machen, wenn man randomise=true nutzt. Es ist sowieso das Wesen des Master/Slave, dass nur ein Broker erreichbar ist.

Komplizierte Netzwerke

Aus geographischen Gründen, oder weil zwei Broker die erhöhte Last nicht tragen können, muss es manchmal eben doch komplizierter sein. Soweit keine „High Availability“ [11] erforderlich ist bzw. Sie nur „High Reliable“ [11] benötigen, lässt sich ein so genanntes Store-and-Forward-Netzwerk aufbauen. Dieses Netzwerk besteht aus mehreren Brokern, die sich wie vorhin beschrieben mittels NetworkConnector verbinden [12]. Falls doch „High Availability“ gewünscht ist, bietet sich eine Kombination aus Master/Slave und Store-and-Forward an, aber da ist schon eine Menge eigene Kreativität gefragt.

Bevor Sie eine so komplizierte Netzwerkstruktur aufbauen, denken Sie über „Traffic Portionierung“ nach. „Traffic Portionierung“ bedeutet, dass eine konkrete Entscheidung getroffen wird, an welchem Broker sich welche Queue befindet. Ideal wäre, wenn sich unterschiedliche Gruppen von Clients mit unterschiedlichen Brokern verbinden könnten. Das geht jedoch nicht immer, und das könnte unter Umständen bedeuten, dass ein Client nicht nur einen Connection-String, sondern einen Connection-String für jede Gruppe von Queues hat (wie vorhin bei der Clientkonfiguration erklärt). Eine „Traffic Portionierung“-Brokerlandschaft besteht also aus mehreren voneinander isolierten Broker-Paaren.

Fazit

Zuerst haben wir Grundbegriffe aus ActiveMQ bzw. JMS erklärt und die häufigsten Konfigurationsänderungen (Protokoll, persistente Nachrichten) beim Broker und dem JMS-Client erläutert. Darüber hinaus haben wir uns mit der Funktionsweise bzw. den Nachrichtenflüssen des ActiveMQ beschäftigt. Abschließend sind wir auf Netzwerke mit mehreren Brokern und die entsprechenden Konfigurationen eingegangen. Eine einfache 2-Broker-Konfiguration wurde veranschaulicht und die Richtung für kompliziertere Brokerlandschaften aufgezeigt. Damit bekommen Sie das Werkzeug in die Hand, um selbst eine umfassende Implementierung von ActiveMQ vorzunehmen.

Geschrieben von
Ivan Mioc

Ivan Mioc arbeitet bei Cirquent und ist seit mehr als zehn Jahren im Java-Umfeld tätig.

Kommentare

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>