PHP Oracle Web Applications: Best Practices and Caching Strategies Kuassi Mensah Oracle Corporation USA Keywords: PHP, OCI* DRCP, Query Change Notification, Query Result Cache, Stored Procedures, Failover. Introduction Web application performance and availability correlates with attracting and keeping users. What are Oracle database 11g built-in mechanisms that help Web application performance and availability? Can a single database instance sustain 20,000 concurrently active web application users? How to scale Web applications and queries? How do Oracle's automatically managed caches improve query time with no architecture changes or new infrastructure? How to upgrade web applications while actively being used? This paper will answer those questions. Scaling Database Connectivity Database connections are expensive to create as it involves OS process spawning, network connection (several roundtrips), session creation and database authentication. Database connections are also expensive to tear down. Allocating a very large number of database connections would require excessive system resources (this is currently the limiting factor); repeatedly connecting/disconnecting can be a huge scaling issue (huge CPU overhead). How to efficiently handle 20K simultaneous PHP users over a single database instance? Database Resident Connection Pool DRCP DRCP is a pool of dedicated servers shared across client systems and processes, as illustrated by figure 1 below. The pooled server is locked on connect, and released on disconnect. The result is a low connect/disconnect costs, low-latency performance of dedicated servers, and extreme scalability. In test environment, we were able to support more than 20,000 connections to a 2 GB Database Server; more details in the following paper: http://www.oracle.com/technology/tech/php/ Pooling is enabled by the DBA using EXECUTE DBMS_CONNECTION_POOL.START_POOL ('SYS_DEFAULT_CONNECTION_POOL'); Change connect string on client in tnsnames.ora:
(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=sales-server)(PORT=1521) (CONNECT_DATA=(SERVICE_NAME=sales)(SERVER=pooled))) The Easy Connect syntax, e.g., sqlplus joeuser@sales-server:1521/sales:pooled can be used as well. Figure 1 DRCP Architecture Configuring the pool: SQL>execute dbms_connection_pool.configure_pool(pool_name => 'SYS_DEFAULT_CONNECTION_POOL', minsize => 4, maxsize => 40, incrsize => 2, session_cached_cursors => 20, inactivity_timeout => 300, max_think_time => 600, max_use_session => 500000, max_lifetime_session => 86400); Starting the pool: SQL> execute dbms_connection_pool.start_pool(); Oracle database 11g R2 furnishes a new Database Resident Connection Pool (DRCP) dynamic performance view V$CPOOL_CONN_INFO. There is a GV$ counterpart for Oracle
RAC. The new view displays information about each connection to the DRCP Connection Broker. This gives more insight into client processes that are connected, making it easier to monitor and trace applications that are currently using pooled servers or are idle. Logon Storm Logon storm consists in a sudden spike in incoming connection rate; which results in CPU starvation for existing sessions. Logon storm happens under normal circumstances, when middle-tiers reboot and all users try to get a connection. Logon storm happens also under abnormal circumstances, during a Denial of Service attack (DoS attack). The Connection Rate Limiter protects from logon storm; it provides end-point level control of throttling and can be configured in LISTENER.ORA LISTENER=(ADDRESS_LIST= (ADDRESS=(PROTOCOL=tcp)(HOST=sales)(PORT=1521)(RATE_LIMIT=3)) (ADDRESS=(PROTOCOL=tcp)(HOST=lmgmt)(PORT=1522)(RATE_LIMIT=no))) Set the Rate Limit to a value that matches your machine capabilities. Scaling Database Operations Once connected, database operations can be summarized in parsing and executing SQL statements, then retrieving query result sets. However, optimizing the performance and scaling database operations for PHP Web applications requires a deeper look, as covered in the following best practices. Scaling with Bind Variables $s1 = oci_parse($c,"select last_name from employees where employee_id = 1894"); oci_execute($s1);...... $s2 = oci_parse($c,"select last_name from employees where employee_id = 11103"); oci_execute($2); The use of literals in SQL statements as illustrated above prevents their sharing resulting in additional hard parses which consumes significant database time. Hard parse is expensive as it creates shared cursor in SGA, causes library cache latch contention, shared pool contention and scalability issues. Best Practices recommend the use of bind variables as illustrated hereafter $s = oci_parse($c,"select last_name from employees where employee_id = :empid"); $empid = 1894; oci_bind_by_name($s, ":EIDBV", $empid);
oci_execute($s);...... $empid = 11103; oci_execute($s); // No need to re-parse Bind variables reduce hard parse on the server and the risk of SQL injection (potential security). If the application cannot be changed to use bind variables, you can force cursor sharing using init.ora parameter: CURSOR_SHARING={FORCE SIMILAR EXACT} Default is EXACT. Scaling with Statement Caching When a session executes a statement in database shared pool, it creates a session specific cursor context (soft parsing) and repeats metadata processing. Best practices recommend the use of client-side statement caching. Client-side statement caching moves cache management load from the database-side to the PHP side. The client (PHP driver) caches the statement handles while the database keeps frequently used session cursors open. Client-side statement cache can be enabled and sized in php.ini oci8.statement_cache_size = 20 Scaling with Stored Procedures As depicted in figure 2 below, stored procedures allow the partitioning of client-side and database-side processing by bundling SQL statements with procedural/algorithmic statements. Figure 2 Stored Procedures The following simple PHP code loops over INSERT statement then commits the transaction. function do_transactional_insert($conn, $array) { $s = oci_parse($conn, 'insert into ptab (pdata) values (:bv)'); oci_bind_by_name($s, ':bv', $v, 20, SQLT_CHR);
} foreach ($array as $v) $r = oci_execute($s, OCI_DEFAULT); oci_commit($con); It takes 8 millisec on my laptop. Each iteration translates into a roundtrip to the database; this code can be optimized to use a stored procedure as follows: PL/SQL Stored package (could be Java) create or replace package mypkg as type arrtype is table of varchar2(20) index by pls_integer; procedure myproc(p1 in arrtype); end mypkg; create or replace package body mypkg as procedure myproc(p1 in arrtype) is begin forall i in indices of p1 insert into ptab values (p1(i)); end myproc; end mypkg; PHP Code function do_bulk_insert($conn, $array) { $s = oci_parse($conn, 'begin mypkg.myproc(:c1); end;'); oci_bind_array_by_name($s, ":c1", $array, count($array), -1, SQLT_CHR); oci_execute($s); } This code takes 2 millisec; everything being equal, that s 4 x speed up. Scaling with ResultSets Prefetching Prefetched rows are cached internally by the driver which reduced database round-trips and improves query performance. The default prefetch size specifies the maximum number of rows in each DB "round trip"; it can be set either in php.ini or inline in PHP code oci8.default_prefetch = 10 oci_set_prefetch($s, 100); The maximum memory allocation is 1024 * oci8.default_prefetch. Scaling with REF_CURSOR PreFetching Oracle database 11g R2 furnishes Ref Cursor pre-fetching. REF CURSORS are like pointers to result sets. Typically, queries are performed in PL/SQL or Java-in-the-database then a REF CURSOR is returned to the caller (i.e., PHP) for processing the results set. Oracle Database 11gR2 allows pre-fetching of rows from REF CURSORs (aka Cursor Variables); this mechanism increases greatly the performance of Oracle PL/SQL and Java stored procedures and functions.
Prefetching minimizes database server round-trips by returning batches of rows to an Oraclemanaged cache each time a request is made to the database. Prefetching was previously only supported for queries. With Oracle Database 11gR2, the default REF CURSOR prefetch row count size is the value of oci8.default_prefetch in php.ini, i.e. 100 in PHP OCI8 1.3. The size can be explicitly changed for a REF CURSOR. For example, to increase the prefetch size of a REF CURSOR to 200: $stid = oci_parse($c, "call myproc(:rc)"); $refcur = oci_new_cursor($c); oci_bind_by_name($stid, ':rc', $refcur, -1, OCI_B_CURSOR); oci_execute($stid); oci_set_prefetch($refcur, 200); oci_execute($refcur); oci_fetch_all($refcur, $res); Beyond stored procedures, Java in the database allows implementing more complex database resident code for accomplishing various services including: sending emails with attachment from within the database, producing PDF files from Result Set, executing external OS commands and external procedures, MD5 CRC, parsers for various file formats (txt, zip, xml, binary), image transformation and format conversion (GIF, PNG, JPEG, etc), databaseresident Content Management System*, HTTP callout, JDBC callout, RMI callout to SAP, Web Services callout, messaging across Tiers, RESTful Database Web Services* and DB Resident Lucene 1. Scaling Very Complex Queries with Cube-Organized Materialized Views With the embedded OLAP engine, Oracle Database 11g allows simple SQL access to complex analytics 2. The cube materialized views are geared for cases where users are performing ad hoc analyses across a wide summary space. The three main goals of Cube MV s are: 1) provide a manageable summary solution where a few cubes can satisfy many (i.e. 1000's) of summary combinations 2) deliver good data preparation performance (i.e. creation of aggregates) and 3) deliver fast query performance for queries that rewrite to the cube. Problem to Solve: Query Sales and Quantity by Year, Department, Class and Country The SQL Query SELECT SUM(s.quantity) AS quantity, SUM(s.sales) AS sales, t.calendar_year_name, p.department_name, c.class_name, cu.country_name FROM times t, products p, channels c, customers cu, sales_fact s WHERE p.item_key = s.product AND s.day_key = t.day_key AND s.channel = c.channel_key AND s.customer = cu.customer_key GROUP BY p.department_name, t.calendar_year_name, c.class_name, cu.country_name; 1 *http://marceloochoa.blogspot.com 2 http://www.oracle.com/technology/products/bi/olap/11g/demos/olap_sql_demo.html
As illustrated in figure 3 below, the simple SQL queries are rewritten under the cover to use the more complex OLAP query and views. Figure 3 Cube-Organized Materialized Views The views are automatically managed by the materialized view refresh system and are transparently accessed by SQL based applications. Access to summary data occurs via automatic query rewrite to the cube; applications remain unchanged, but updates and queries execute much faster. Cube Organized Materialized Views work with a range of business intelligence tools including Oracle Business Intelligence Enterprise Edition, Business Objects, Cognos and Microstrategy. Caching Strategies The fastest database access is no database access; how to implement effective database caching strategies using built-in Oracle database 11g mechanisms? Let s look at Continuous Query Notification, Client-Query Result Cache and In Memory Database cache. Continuous Query Notification Problem to solve: be notified when changes in the database invalidates an existing query result set Continuous Query Notification is an Oracle database mechanism which allows applications such as PHP to: 1) Register the query to subscribe to notification of changes impacting the result set as if the query was continuously being executed 2) Upon DML change that impacts the result set, a notification is sent to subscribers thread or database resident PL/SQL or Java stored procedure as notification handlers. 3) Subscribers can invalidate and repopulate the cache
Client-Query Result Cache (CQRC) MemCached is a popular distributed open source query result caching mechanism; it stores stringfied the result set in a shared cache using a (key, value) pair where it can be retrieved query = "select name, address, phone, acctbal from custumer, nation where c_nationkey= n_nationkey; key = md5($query); If (serval=$memcache->get($key) { res = oci_execute($query) ser = serialize($res); memcache->set($key, $ser); } res = unserialize($serval); However, MemCached has not built-in mechanism for detecting that the cached data has changed in the database and invalidates the cache. In addition, MemCached requires additional servers to sustain the distributed shared cache. Oracle Database 11g Client-side Query Result Cache allows caching the result set in the driver s memory using either query annotation or table annotation as described below. Query Annotation The /* + RESULT_CACHE */ hint to declare a query cache worthy $query = "select /*+ RESULT_CACHE */ name, address, phone, acctbal from customer, nation where c_nationkey=n_nationkey; Table Annotation New in Oracle database 11g R2), table annotation is a non-intrusive DML to declare a table and related queries cache worthy. ALTER TABLE <table-name> RESULT_CACHE (MODE FORCE); The beauty of CQRC is the integration with Continuous Query Notification; as a result the cache is automatically invalidated upon committed changes in the database that impacts the result set. Internal tests show up to 8 x speed up. CQRC is available with all OCI related APIs, drivers and adapters including: C(OCI), C++ (OCCI), PHP OCI8, Ruby-oci8, Python cx-oracle, ODP.Net, ODBC, JDBC-OCI. In Memory Database Cache (a.k.a. TimesTen) Based on In-Memory Database technology; the entire database is always in memory, which furnishes up to 10 x speed. Applications can embed in-memory database into their process address space which, eliminates network and inter-process communication overhead as well as a very low response time (like calling a procedure). In Oracle Database 11g, IMDB supports C/C++ (OCI) along with Java. By the time of this writing, Oracle is looking into the possibility of supporting PHP OCI8 with IMDB. IMDB support with PHP will represent a breakthrough in terms of caching strategies.
Contact address: Kuassi Mensah Oracle Corporation 400 Oracle Parkway 94065, Redwood City, USA Phone: (+1) 650 607 2229 Fax: (+1) 650 506 7225 Email : kuassi.mensah@oracle.com Blog : http://db360.blogspot.com