
BTP - Schnittstellen Performance
Welche Schnittstellentechnologie hat aktuell die beste Performance beim Zugriff auf On-Premise Daten in ABAP? Hier gehen wir mehr in die Details.
Inhaltsverzeichnis
In diesem Artikel schauen wir uns die Performance von verschiedenen Technologien und Protokollen an und vergleichen die Laufzeit in verschiedenen Szenarien miteinander. Am Ende schauen wir auf das Ergebnis und leiten daraus weitere Schlussfolgerungen ab.
Einleitung
Hast du dich schon einmal gefragt, welche Technologie für den Zugriff auf Daten in einem On-Premise System die besten und schnellsten sind? Aktuell gibt es im SAP Bereich die verschiedenen Möglichkeiten Daten zu verteilen oder auch bereitzustellen. Da wären zum Beispiel der klassische RFC, SOAP für modernere Schnittstellen, der neue Standard REST und OData oder für die Verteilung von Belegen und Stammdaten IDoc. Heute schauen wir uns den Zugriff auf On-Premise Daten und die damit verbundene Laufzeit an.
On-Premise
Bevor wir mit dem Zugriff beginnen können, müssen wir unser On-Premise System erst noch vorbereiten. Dazu legen wir eine Tabelle an und stellen die Daten nach Außen zur Verfügung.
Tabelle
Die Tabelle befüllen wir mit unterschiedlichen Daten und Datentypen und verwenden dabei als Schlüssel eine UUID. Daneben haben wir zufällig generierte Inhalte und ein String Feld mit zufälligen Zeichenketten. Insgesamt sollen die Daten eine breite Vielfalt an Daten abdecken.
@EndUserText.label : 'Performance Test'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zbs_dmo_perf {
key client : abap.clnt not null;
key identifier : sysuuid_x16 not null;
item_description : abap.char(40);
description : abap.char(150);
@Semantics.amount.currencyCode : 'zbs_dmo_perf.currency'
amount : abap.curr(15,2);
currency : abap.cuky;
blob : abap.string(0);
ndate : abap.dats;
ntime : abap.tims;
utc : abap.utclong;
}
Initialisierung
Die Datenerzeugung übernimmt eine ausführbare Klasse für uns. Dabei wollen wir zufällige Daten erzeugen, bei denen die Datensätze recht unterschiedlich sind. Damit wollen wir eine Komprimierung bzw. Verdichtung beim Datentransfer vermeiden, wenn du viele gleiche Datensätze existieren.
CLASS zcl_bs_demo_fill_performance DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PRIVATE SECTION.
DATA randomizer_amount TYPE REF TO zcl_bs_demo_random.
DATA randomizer_currency TYPE REF TO zcl_bs_demo_random.
DATA randomizer_text TYPE REF TO zcl_bs_demo_random.
DATA randomizer_blob TYPE REF TO zcl_bs_demo_random.
DATA randomizer_date TYPE REF TO zcl_bs_demo_random.
DATA randomizer_time TYPE REF TO zcl_bs_demo_random.
DATA all_letters TYPE string.
METHODS initialize_class_attributes.
METHODS get_random_currency
RETURNING VALUE(result) TYPE waers.
METHODS get_description
RETURNING VALUE(result) TYPE zbs_dmo_perf-description.
METHODS get_blob
RETURNING VALUE(result) TYPE zbs_dmo_perf-blob.
METHODS get_letter
RETURNING VALUE(result) TYPE string.
METHODS get_date
RETURNING VALUE(result) TYPE d.
METHODS get_time
RETURNING VALUE(result) TYPE t.
ENDCLASS.
CLASS zcl_bs_demo_fill_performance IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DATA new_database_entries TYPE STANDARD TABLE OF zbs_dmo_perf WITH EMPTY KEY.
initialize_class_attributes( ).
DELETE FROM zbs_dmo_perf.
COMMIT WORK.
DO 50 TIMES.
DO 500 TIMES.
DATA(new_entry) = VALUE zbs_dmo_perf( identifier = xco_cp=>uuid( )->value
description = get_description( )
amount = randomizer_amount->rand( )
currency = get_random_currency( )
blob = get_blob( )
ndate = get_date( )
ntime = get_time( )
utc = utclong_current( ) ).
new_entry-item_description = |Item from { new_entry-ndate DATE = USER } in currency { new_entry-currency }|.
INSERT new_entry INTO TABLE new_database_entries.
ENDDO.
INSERT zbs_dmo_perf FROM TABLE @new_database_entries.
COMMIT WORK.
CLEAR new_database_entries.
ENDDO.
out->write( |New Entries created in ZBS_DMO_PERF| ).
ENDMETHOD.
METHOD initialize_class_attributes.
all_letters = to_lower( sy-abcde ) && to_upper( sy-abcde ).
randomizer_amount = NEW zcl_bs_demo_random( id_min = 2
id_max = 450 ).
randomizer_currency = NEW zcl_bs_demo_random( id_min = 1
id_max = 4 ).
randomizer_text = NEW zcl_bs_demo_random( id_min = 1
id_max = 52 ).
randomizer_blob = NEW zcl_bs_demo_random( id_min = 500
id_max = 2000 ).
randomizer_date = NEW zcl_bs_demo_random( id_min = 0
id_max = 730 ).
randomizer_time = NEW zcl_bs_demo_random( id_min = 0
id_max = 360 ).
ENDMETHOD.
METHOD get_random_currency.
CASE randomizer_currency->rand( ).
WHEN 1.
result = 'EUR'.
WHEN 2.
result = 'USD'.
WHEN 3.
result = 'RUB'.
WHEN 4.
result = 'CHF'.
ENDCASE.
ENDMETHOD.
METHOD get_description.
DO 150 TIMES.
result &&= get_letter( ).
ENDDO.
ENDMETHOD.
METHOD get_blob.
DO randomizer_blob->rand( ) TIMES.
result &&= get_letter( ).
ENDDO.
ENDMETHOD.
METHOD get_letter.
result = substring( val = all_letters
off = CONV i( randomizer_text->rand( ) - 1 )
len = 1 ).
ENDMETHOD.
METHOD get_date.
RETURN sy-datum - randomizer_date->rand( ).
ENDMETHOD.
METHOD get_time.
RETURN sy-uzeit - randomizer_time->rand( ) * 60.
ENDMETHOD.
ENDCLASS.
Die Klasse erzeugt 25000 Datensätze, jeweils in Blöcken von 500 Einheiten, die an die Datenbank übergeben werden. Damit wollen wir verhindern, dass wir in einem Schritt eine sehr große Anzahl von Einträgen übergeben. Nach Ausführung der Klasse finden wir in der Tabelle unsere Daten.
RFC Funktionsbaustein
Der erste Typ Schnittstelle ist ein klassischer RFC Funktionsbaustein. An den Baustein übergeben wir eine Liste von Feldern, die wir Lesen wollen und einen TOP/SKIP, damit wir Teile der Datenmenge lesen können. Bevor du allerdings die generische Liste einfach an das SELECT Statement übergibst, solltest du den Inhalt prüfen. Dazu verwenden wir eine Standardklasse, die die Überprüfung für uns durchführt.
FUNCTION z_bs_demo_get_performance_data
IMPORTING
VALUE(selection_fields) TYPE string_table
VALUE(top) TYPE i
VALUE(skip) TYPE i
EXPORTING
VALUE(performance_data) TYPE zbs_t_demo_performance.
DATA(fields_for_select) = ``.
LOOP AT selection_fields REFERENCE INTO DATA(field_name).
DATA(validated_name) = cl_abap_dyn_prg=>check_column_name( val = field_name->*
strict = abap_true ).
IF fields_for_select <> ``.
fields_for_select &&= `, `.
ENDIF.
fields_for_select &&= validated_name.
ENDLOOP.
IF fields_for_select IS INITIAL.
fields_for_select = `*`.
ENDIF.
SELECT FROM zbs_dmo_perf
FIELDS (fields_for_select)
ORDER BY identifier
INTO CORRESPONDING FIELDS OF TABLE @performance_data
UP TO @top ROWS
OFFSET @skip.
ENDFUNCTION.
Hinweis: Neben der Prüfung der übergebenen Felder, sollte im Falle eines RFC Funktionsbausteins auch eine sinnvolle Berechtigungsprüfung erfolgen, um die Daten zu schützen (Best Practice).
OData Service
Im nächsten Schritt wollen wir einen OData Service zur Verfügung stellen. Dabei schauen wir uns das Protokoll Version 2 und Version 4 an. Dazu legen wir auf unserer Tabelle einen Basis View an und normalisieren die Felder.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Base View Performance Data'
define view entity ZBS_B_DMOPerformance
as select from zbs_dmo_perf
{
key identifier as Identifier,
item_description as ItemDescription,
description as RandomDescription,
@Semantics.amount.currencyCode: 'Currency'
amount as Amount,
currency as Currency,
blob as BlobObject,
ndate as NewDate,
ntime as NewTime,
utc as UTCTimestamp
}
Dazu generieren wir einen Root View und binden diesen dann in eine Service Definition ein. Da wir hier kein Verhalten benötigen, sparen wir uns die Verhaltensdefinition und -implementierung.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Root Entity Performance'
define root view entity ZBS_R_DMOPerformance
as select from ZBS_B_DMOPerformance
{
key Identifier,
ItemDescription,
RandomDescription,
Amount,
Currency,
BlobObject,
NewDate,
NewTime,
UTCTimestamp
}
Darüber generieren wir uns dann zwei Service Bindings und geben eine OData API nach außen für die Protokolle OData v2 und OData v4.
ABAP Environment
Im nächsten Schritt müssen wir die Schnittstellen im ABAP Environment einbinden und entsprechende Objekte (Consumption Model) für die Konsumierung anlegen. In allen Beispielen verwenden wir den Destination Service in der BTP, um auf die Daten zuzugreifen. Die aktuelle Best Practice ist im Moment aber die Anlage eines Communication Arrangement und eines Communication Systems.
Funktionsbaustein
Über die ACO_PROXY ziehen wir uns die Definition des Funktionsbausteins, um die Metadaten dann einzuspielen. Wie du einen RFC Funktionsbaustein einbindest und aufrufst, erfährst du in diesem Artikel. Im Anschluss legen wir ein Consumption Model über die Metadaten ein und bauen den Aufruf auf.
DATA(destination) = cl_rfc_destination_provider=>create_by_cloud_destination( destinations-rfc ).
DATA(consumption_model) = NEW zcl_bs_demo_intf_func( destination = destination ).
consumption_model->z_bs_demo_get_performance_data( EXPORTING selection_fields = request_data-fields
skip = request_data-skip
top = request_data-top
IMPORTING performance_data = result-data_rfc ).
OData
Über den Service speichern wir uns für beide Protokolle die Metadaten mit dem Zusatz "$metadata". Wie du einen OData Service im ABAP Environment einbinden und konsumieren kannst, erfährst du in diesem Artikel. Im Anschluss implementieren wir den Aufruf für die beiden Arten von Aufrufen. Diese unterscheiden sich bei der Instanziierung des Objekts, sind aber sonst recht identisch.
DATA(destination) = cl_http_destination_provider=>create_by_cloud_destination( destinations-http ).
DATA(http_client) = cl_web_http_client_manager=>create_by_http_destination( destination ).
DATA(client_proxy) = /iwbep/cl_cp_factory_remote=>create_v2_remote_proxy(
is_proxy_model_key = VALUE #( repository_id = 'DEFAULT'
proxy_model_id = 'ZBS_DEMO_INTF_O2'
proxy_model_version = '0001' )
io_http_client = http_client
iv_relative_service_root = '/sap/opu/odata/sap/ZBS_API_PERFROMANCE_O2/' ).
DATA(request) = client_proxy->create_resource_for_entity_set( 'PERFROMANCE' )->create_request_for_read( ).
request->set_top( request_data-top )->set_skip( request_data-skip ).
request->set_select_properties( request_data-fields ).
DATA(response) = request->execute( ).
response->get_business_data( IMPORTING et_business_data = result-data_o2 ).
Plain HTTP
Als letztes Szenario implementieren wir einen Plain HTTP Aufruf, das heißt wir verwenden nicht das Consumption Model und auch nicht die Gateway Komponenten im System.
DATA(destination) = cl_http_destination_provider=>create_by_cloud_destination( destinations-http ).
DATA(http_client) = cl_web_http_client_manager=>create_by_http_destination( destination ).
DATA(fields_for_select) = ``.
LOOP AT request_data-fields REFERENCE INTO DATA(field_name).
IF fields_for_select <> ``.
fields_for_select &&= `,`.
ENDIF.
fields_for_select &&= field_name->*.
ENDLOOP.
DATA(uri) = `/sap/opu/odata4/sap/zbs_api_perfromance_o4/srvd_a2x/sap/zbs_demo_perfromance_data/0001/Perfromance?`.
uri &&= |$select={ fields_for_select }&$top={ request_data-top }&$skip={ request_data-skip }|.
DATA(request) = http_client->get_http_request( ).
request->set_uri_path( i_uri_path = uri ).
DATA(response) = http_client->execute( if_web_http_client=>get ).
DATA(status) = response->get_status( ).
IF status-code = 200.
/ui2/cl_json=>deserialize( EXPORTING json = response->get_text( )
CHANGING data = json_payload ).
result-http_payload = json_payload-value.
ENDIF.
Die Logik ist dabei länger, da wir uns um alles selbst kümmern müssen. Zuerst müssen wir die URI zusammenbauen und die Feldliste, sowie TOP und SKIP, befüllen. Im Anschluss mappen wir das Ergebnis auf unsere interne Struktur. Dabei verwenden wir den OData v4, da dieser im Standard JSON als Ergebnis liefert.
Test
In diesem Kapitel schauen wir uns die verschiedenen Testszenarien an und was wir testen wollen.
Testfälle
Bei unserem Test wollen wir uns vier Zugriffsmethoden anschauen, per RFC mit Consumption Model, OData v2 und OData v4 mit Consumption Model, sowie per Plain HTTP, bei dem wir die Anfrage erstellen und das Mapping durchführen. Dazu testen wir verschiedene gängige Szenarien zur Datenbeschaffung:
- Alle Felder und Lesen von 100 Einträge
- Alle Felder und Lesen von 5000 Einträgen
- Ohne BLOB (RandomDescription, BlobObject) und Lesen von 5000 Einträgen
- Ohne BLOB (RandomDescription, BlobObject), viele Ausführungen und Lesen von einem 200er Block (gleiche Datensätze)
- Ohne BLOB (RandomDescription, BlobObject), viele Ausführungen und Lesen von 200er Blöcken
- Wertehilfe (Schlüssel, Beschreibung) und Lesen von 500 Einträgen
- Wertehilfe (Schlüssel, Beschreibung), viele Ausführungen und Lesen von einem 20er Block
- Wertehilfe (Schlüssel, Beschreibung), viele Ausführungen und Lesen von 20er Blöcken
Annahme
Mit welcher Annahme würden wir nun in den Test gehen? Grundsätzlich würden wir sagen, dass RFC immer noch das schnellere Protokoll ist, da weniger Mapping, Aufbereitung und Checks bei der Ausführung durchgeführt werden müssen. Zum anderen sollte Plain HTTP schneller als OData sein, da eine große Schicht ABAP wegfällt. Beim Thema Performance zwischen OData v2 und OData v4 wird es spannend. Grundsätzlich wird OData v4 als JSON übertragen, was die Payload kleiner macht.
Durchführung
Wenn du in das Repository schaust, verwenden wir einen HTTP Endpunkt um die Logik zu testen. Damit haben wir die Möglichkeit per Principal Propagation und per Basic Authentication zu testen. Je nachdem wie du den Destination Service verwenden willst oder welche Verbindung du prüfen möchtest.
Die Payload haben wir in verschiedene Teiltest unterteilt, da wir immer wieder feststellen mussten, dass einzelne Läufe plötzlich weit außerhalb der anderen Läufe lagen. Das Verhalten hatten wir uns nicht im Detail angeschaut, dazu ein Beispiel:
Ergebnisse
Hier die zusammengefassten Testläufe. Für einen Durchschnitt wurden jeweils drei Läufe durchgeführt, sodass du auch die oben beschriebenen Abweichungen erkennen kannst. Scheinbar tritt das Phänomen bei vielen Aufrufen des HTTP Frameworks auf.
Testfälle 1 und 2:
Testfälle 3 und 4:
Testfälle 5 und 6:
Testfälle 7 und 8:
Auswertung
Welche Erkenntnisse nehmen wir aus dem Test und den Testergebnissen mit? Hier einmal eine kurze Zusammenfassung der Punkte aus dem Test:
- RFC ist nach wie vor das einfachste und schnellste Protokoll, wenn es um das Lesen und Verschieben von Daten und Mengen geht.
- Eine hohe Frequenz von HTTP Abfragen führt zu längeren Wartezeiten, hier am besten wenige Anfragen mit großen Datenmengen verwenden.
- Plain HTTP Aufrufe sind beim Lesen und Parsen schneller als durch die OData Integration zu laufen.
- Beim einmaligen Aufruf der Schnittstelle sind alle Technologien recht nah zusammen und können einfach genutzt werden.
- Die Implementierung von HTTP und OData benötigt mehr Quellcode, um einfache Leseaktionen zu integrieren.
Zusammenfassung
In der Vergangenheit gab es für den Transfer von sehr großen Datenmengen die Dateiverarbeitung oder den direkten Aufruf von RFC Funktionsbausteinen, bei denen wir uns Teilmengen der Datensätze holen konnten. Die aktuelle Clean Core Strategie bewegt sich Richtung OData und SOAP Services. Die Empfehlung auf die klassischen Schnittstellen wie RFC oder IDoc zu vermeiden, sollte in Performance kritischen Situationen immer mit geprüft werden.
Grundsätzlich ist RFC immer noch das schnellste Protokoll, wenn es um eine einfache Übertragung von Daten und Inhalten geht. Die Performance der OData Anbindung liegt vor allem in der Wrapper Schicht, die zwischen HTTP Framework und ABAP liegt und das Handling der Anfragen durchführt, aber auch zur Stabilität und Sicherheit beiträgt.
Vollständiges Beispiel
Die vollständigen Ressourcen findest du in verschiedenen GitHub Repositorys, einmal für den On-Premise und einmal für den ABAP Environment Teil. Die nicht gelisteten Hilfsklassen findest du in einem eigenen Repository, da wir sie immer wieder in unterschiedlichen Projekten verwenden.
Fazit
Mit diesem Test wollten wir dir das Thema Performance näherbringen und wie die aktuellen Technologien im Vergleich zum Klassiker aussehen. Dabei haben wir uns Events und SDA nicht angeschaut, um erst einmal bei den weit verbreiteten Techniken für direkten Zugriff zu bleiben.